计算机图形学 实验三 相机、阴影、光照

实验3.1 相机定位

一、 实验目的

  1. 了解OpenGL中观察变换(模视变换和投影变化)的基本原理
  2. 掌握OpenGL中相机观察变换矩阵的推导
  3. 掌握OpenGL中实现相机定位观察变换

二、 理论背景

1.各个坐标系

坐标系这里没完全搞懂 建议先别看我的 可以直接看openGL的别人怎么写的:
https://learnopengl-cn.readthedocs.io/zh/latest/01%20Getting%20started/08%20Coordinate%20Systems/
在讲述相机之前,这里额外补充几个坐标系的内容,在之后会用到在坐标系间的切换:
在这里插入图片描述

世界坐标系

即在场景中的绝对坐标,通过世界坐标系的坐标值可以定位任何一个地方的物体
在这里插入图片描述

相机坐标系

在这里插入图片描述
总结下有这几个属性:

  1. 相机坐标系的初始位置位于原点,默认世界坐标系与相机坐标系重合。
  2. 相机位于原点,无法观察同样位于原点的模型。
  3. 相机朝向z轴负向,因此如果观察的某对象需要在z轴正方向的投影平面(在OpenGL中通常是近裁剪平面)上成像,那么必须要将相机沿着z轴正方向往后移动一定距离。
  4. 当移动相机时,相机所在的坐标系也会跟着移动(因此有时就需要坐标系的转换)
    相机还具有这两个属性:VUP和VPN
    在这里插入图片描述
    **观察平面法向量**(VPN:View-plane Normal)和**观察正向向量**(VUP:View-up Vector)。
    其中VPN指定了相机的胶片平面,而平面是由其法向量决定的

但是如果只是指定了VPN,相机还可以绕VPN旋转,此时我们再加上VUP,就能完全固定一个相机的位置了。

VUP和VRP这两个属性会随着相机的旋转而发生变化。换言之,只有确定了这两个属性才能完全确定。

在这里插入图片描述

但是在OpenGL中我们习惯用目标点减去眼睛的方向向量来获得VPN,详细见面的内容。

模型视图矩阵

说实话这里并没有搞很懂,以后哪天来仔细研究吧

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这个矩阵构造起来很麻烦,但是我们可以写一个函数来实现,就是LookAt函数,可以见“三、实验内容”有讲函数的推导和使用。

我们使用LookAt函数传入eye at 和up
前两个参数分别是眼睛和目标点(都是世界坐标系),up是世界坐标系下的向量(0,1,0)
虽然这里理解的不是很懂,但是应该 可以简单的理解为
在这里插入图片描述

“对于相机坐标,我们本质上是将世界坐标进行平移和旋转,然后让它正确地显示出从相机视角观察的特性。我们同样通过一个变换矩阵来将世界坐标转换为相机坐标:”
其实就是由LookAt函数实现了世界坐标系到相机坐标系下的切换。

ndc坐标系

Normalized Device coordinate System(标准化设备坐标),因为不同显示器长宽比不同,我们没办法以像素来衡量他们的坐标。因此我们利用百分比的思想,将坐标归一化到 [-1, 1] 的范围内。
在这里插入图片描述
但是这里需要注意一点的是,在上篇博客所讨论到的mouse函数中传入的x和y,并不是这里的x和y,而是传入的是一个像素值,(坐标轴在原点,范围为0到500)

淦 下面这图的坐标系好像写错了,变换推导大概没错,坐标系的知识有点复杂

想把这个像素值转化为-1到1的坐标系的话,可以通过以下函数来转化:这里节选自我计图实验的推导写的,(随便写的太丑别介意)
在这里插入图片描述

然后在mouse函数中修改:
在这里插入图片描述

2. 齐次坐标

首先回顾一下齐次坐标的概念,详细内容请参考实验2.3。
3维数据可以通过3维向量与3×3矩阵的乘法操作,来完成缩放和旋转的线性变换,也就是说三维空间的缩放矩阵和旋转矩阵都可以采用3维方阵表示。但是对3维坐标的平移是无法通过这样类似的操作完成,此时我们还需要额外的一个向量,将三维空间中的点移动到另外的位置,如下表示。
在这里插入图片描述
这里将原始点加上第四个维度,就是齐次坐标的表示。

三维空间中的点的坐标通过将齐次坐标中前三维度的坐标值除以最后一个维度值得到。

在后面章节中将会看到,齐次坐标多出的第四分量是用来实现透视投影变换的。

3. OpenGL观察变换

计算机图形学中的观察是建立在虚拟照相机模型的基础上,如下左图所示。而下右图则是对虚拟照相机模型的抽象表示,其本质上都包含有相同的元素:对象、观察者、投影线和投影平面。投影线相交于投影中心(COP:Center of Projection),而COP对应于人眼或者相机镜头,从COP点发出投影光线将物体投影在投影平面上而成像。所以在虚拟照相机模型中,如果需要正确的表现出投影对象在投影平面上的成像效果,我们必须首先确定相机的位置和方向。
在这里插入图片描述

在前面的章节中,确定了这样一个事实:只要顶点着色器输出的顶点位于裁剪体(或视见体)内部,那么这些顶点会被送入绘制流水线后面的光栅化模块。如果我们加入自定义的相机姿态,那么对相机的控制则尤为重要,因为相机的位置和朝向将决定怎么发出投影射线而将物体投影到投影平面上。
为了使得观察过程更加灵活,我们把它分解成两个基本的操作。

  1. 首先,必须设置照相机的位置和方向,该操作由模-视变换来完成,当顶点经过该变换之后将位于相机坐标系中。
  2. 然后是使用投影变换,也就是说把指定的投影(正交投影或透视投影)应用到顶点上。并将默认的视见体内部对象变换到指定的裁剪立方体的内部,整个过程如下图所示。
    在这里插入图片描述
    这一节中我们主要考虑相机的观察变换。所谓变换就是设置一个矩阵,然后乘以三维顶点的坐标(考虑齐次坐标系),得到变换之后的坐标值。正如上述,模-视变换将顶点变换到相机坐标系,所以相机的观察变换必须要包含相机的位置和朝向等信息。
    模-视变换是建模变换和观察变换的级联,通常建模变换矩阵是将对象变换到世界坐标,而观察变换将世界坐标转换到相机坐标。一般来说,建模变换矩阵是单位阵,我们不需要额外处理。即,
    在这里插入图片描述
    而对于照相机来说,其初始方向通常指向z轴负方向,这样才能看见位于相机前方的模型。考虑一个位于原点的对象,由于照相机初始位置也在原点,方向指向z轴负方向,如果该对象需要在z轴正方向的投影平面(在OpenGL中通常是近裁剪平面)上成像,那么必须要将相机沿着z轴正方向往后移动一定距离。
    相机移动之后的位置称为观察参考点(VRP,View Reference Point)。此时,我们考虑采用规范化变换(Normalization Transformation)来表示相机坐标系并指定相机的位置,可以分为两个部分:
    观察平面法向量(VPN:View-plane Normal)和观察正向向量(VUP:View-up Vector)。

一点两向量

其中VPN指定了相机的胶片平面,而平面是由其法向量决定的

在这里插入图片描述

所以通过指定VRP,VPN和VUP可以定位照相机。
但通常在OpenGL中,我们更倾向于在对象坐标系下,采用如下图所示的定位方法,将相机放置于 的位置上称为视点,相机指向的另一个点 称为参考点。这两个点就确定了VPN和VRP,即:
在这里插入图片描述
归一化得到:
在这里插入图片描述
(注意,这里需要加上一个负号)
然后通过VUP和VPN生成与它们都垂直的方向向量:
在这里插入图片描述
最后再计算得到VUP在照相机胶片平面上的投影:
在这里插入图片描述
在这里插入图片描述
这样通过采用u,n和v即可定义出相机的观察变换矩阵(其实就是相机的局部坐标系)如下:
在这里插入图片描述
由于在最开始,我们还需要将相机从坐标原点移动到视点,所以还需要一个平移矩阵如下:
在这里插入图片描述
所以最终的相机观察矩阵为:
在这里插入图片描述
结合模型变换矩阵和相机观察矩阵之后得到模-视变化矩阵如下:
在这里插入图片描述

三、 实验内容

实验内容主要是设计并实现相机定位观察变换接口函数如下。

mat4 void lookAt(vec4 eye, vec4 at, vec4 up)
或
mat4 void lookAt(float eyex, float eyey, float eyez, float atx, float aty, float atz, float upx, float upy, float upz)

请按照如下说明顺序实现并提交作业。
a) 按照实验1.1配置环境,具体请参考实验1.1相关文档。注意:会编译出错,因为缺少文件“TriMesh.h”,请在Blackboard下载新的include文件夹(部分其他文件有更新),将其拷贝到你的include文件夹中。后续实验都会用到文件,请更新好你的include文件夹。
b) 读取cube.off模型
c) 构造相机观察矩阵,并传到顶点着色器中,作用于几何体。

mat4 lookAt( const vec4& eye, const vec4& at, const vec4& up )
    {
			// 请参考上述公式推导过程,将该部分补全
}

	// init():从顶点着色器中获取模-视变换矩阵位置
	modelViewprojectMatrixID = glGetUniformLocation(programID, "modelViewprojectMatrix");

	// display():计算相机观察矩阵并传入顶点着色器
	mat4 modelMatrix = mat4(1.0);
	mat4 viewMatrix = lookAt(eye, at, up);
  	mat4 projectMatrix = mat4(1.0);
	mat4 modelViewprojectMatrix = projectMatrix * viewMatrix * modelMatrix;
	glUniformMatrix4fv(modelViewprojectMatrixID, 1, GL_TRUE, &modelViewprojectMatrix[0][0]);

d) 实现键盘控制,分别改变相机视点,参考点和向上方向以观察不同参数对结果的影响。具体控制可参考教材P157-158页代码或者P434页代码。

LookAt函数的推导

在这里插入图片描述
在这里插入图片描述
由于我们移动物体,可以通过移动相机的负距离来实现,因此我们可以通过对相机的model矩阵乘以一个这个矩阵来实现
在这里插入图片描述
在这里插入图片描述

四、 示例和练习

a) 完成上述基本内容。
b) 实现键盘控制,改变相机视点,参考点和向上方向等,并观察输出。
c) 提示:正方体不显示在窗口或不在窗口中心。可能原因:忘填路径;lookAt()返回的矩阵不对;括号前忘写vec4(大概率,且编译能通过);eye,at,r,θ等参数初始值不对。

其实观察off文件可以发现这个正方体是在1的范围内的,并且还是在原点:
在这里插入图片描述
于是乎我们可以在半径为1的单位圆上从各个角度来观察正方体。
我们参照课本157页和434页的代码:设置eye的参数为这样:
在这里插入图片描述

	vec4 eye(radius * sin(theta) * cos(phi),
		radius * sin(theta) * sin(phi),
		radius * cos(theta),
		1.0);//参考课本模型

于是需要初始化全局变量:

float theta = 0.0;
float phi = 0.0;

在键盘响应函数中这样来修改上面的参数:

void keyboard(unsigned char key, int x, int y)
{
	
	// Todo:键盘控制相机的位置和朝向
	switch(key) 
	{
	case 033:	// ESC键 和 'q' 键退出游戏
		exit(EXIT_SUCCESS);
		break;
	case 'q':
		exit (EXIT_SUCCESS);
		break;
	case 'x': l *= 1.1; r *= 1.1; break;
	case 'X': r *= 0.9; r *= 0.9; break;
	case 'y': b *= 1.1; t *= 1.1; break;
	case 'Y': b *= 0.9; t *= 0.9; break;
	case 'z': n *= 1.1; f *= 1.1; break;
	case 'Z': n *= 0.9; f *= 0.9; break;//这几个参数是perspective所用到的参数,详细下节在内容
	case 'r': radius *= 2.0; break;
	case 'R': radius *= 0.5; break;
	case 'o': theta += dr; break;
	case 'O': theta -= dr; break;
	case 'p': phi += dr; break;
	case 'P': phi -= dr; break;

	case 't':  // 重置所有的属性为初始值
		l = -1.0;
		r = 1.0;
		b = -1.0;
		t = 1.0;
		n = -1.0;
		f = 1.0;
		radius = 1.0;
		theta = 0.0;
		phi = 0.0;
		break;
	}
	glutPostRedisplay();
}

在display函数中,我们只需要设定好eye at(原点)和up(固定数值),然后调用lookat函数生成view矩阵即可。

void display()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glUseProgram(programID);

	// TODO 设置相机参数
	vec4 eye(radius * sin(theta) * cos(phi),
		radius * sin(theta) * sin(phi),
		radius * cos(theta),
		1.0);//参考课本模型
	vec4 at(0.0, 0.0, 0.0, 1.0);//物体所在的地方
	vec4 up(0.0, 1.0, 0.0, 0.0);//观察正向向量VUP
	
	Camera::modelMatrix = mat4(1.0);
	Camera::viewMatrix = Camera::lookAt(eye, at, up);//调用lookat函数来构造view矩阵
	
	Camera::projectMatrix = Camera::perspective(45.0, 1.0, 1.0, 100.0);
	//调用perspective来实现透视投影矩阵
	mat4 modelViewprojectMatrix = Camera::projectMatrix * Camera::viewMatrix * Camera::modelMatrix;
	glUniformMatrix4fv(modelViewprojectMatrixID, 1, GL_TRUE, &modelViewprojectMatrix[0][0]);
}

关于此处涉及到的投影矩阵 :perspective的是下节实验的内容,这里只需要知道加入这个矩阵可以得到好的效果,不会出现被裁切的情况。

随后我们运行 按下o和p键就可以实现立方体的转动。

在这里插入图片描述

实现相机的移动

相机的平移

相机的移动无法像物体一样直接乘以矩阵来实现,但是我们知道构造view矩阵的lookAt函数有三个参数,eye,at,up
其中up一般来说是固定的(0,1,0),

但是在某些情况下并不能设置为(0,1,0),这是在我3.4的实验中,从y轴正上方观察小球你就会发现,此时什么都看不到了,这是因为此时从y轴正向朝下看,那么at就变成了(0,-1,0),此时由于两个向量在一条直线上,则无法实现向量叉乘了,那么计算view矩阵就会出问题,(除了修改up矩阵的方法暂时不知道怎么解决

但是我们可以修改eye和at矩阵,eye就是相机位置,at就是我们所看的方向。
而eye是一个三维向量,我们简单的修改其xyz值就可以实现相机的移动了。
所以初始时我们设定好三个变量:

vec4 cameraPosition(0.0, 0.0, 0.0, 1.0);
vec4 cameraDirection(0.0, 0.0, 2.0, 0.0);
vec4 cameraUp(0.0, 1.0, 0.0, 0.0);

在lookAt函数中传入的也是这三个数值:

mat4 view=lookAt(cameraPosition, cameraPosition + cameraDirection, cameraUp);

然后通过键盘函数来实现position的变换,

void keyboard(unsigned char key, int x, int y)
{
	switch(key) 
	{
	case 'w':
		cameraPosition.z -= 0.05f;
		//为什么这里是减?因为初始时我们是面对z轴负方向的
		break;
	case 's':
		cameraPosition.z += 0.05f;
		break;
	case 'a':
		cameraPosition.x -= 0.05f;
		break;
	case 'd':
		cameraPosition.x += 0.05f;
	case ' ':
		cameraPosition.y += 0.05f;
	case 'c':
		cameraPosition.y -= 0.05f;
	}
	glutPostRedisplay();
}

此时我们就会观察到可以通过键盘控制来移动物体:
起始:
在这里插入图片描述
现在:
在这里插入图片描述
在这里插入图片描述

第一人称相机

仅仅是平移相机是十分乏味的,接下来实现第一人称游戏所用的相机。
在此之前需要先学一个概念欧拉角:

欧拉角

一张图带你了解欧拉角:
在这里插入图片描述
我们联系现实,其中ψ可以理解为我们现实中往前后左右转向所转的角度,而θ角就是我们抬头和低头。
至于Φ角,其实就是你歪脖子看世界所转的角度。

由于此处只实现简单的第一人称射击游戏,故不深入探讨欧拉角,有关于旋转的其他表示方法,欧拉角的缺陷(万向节死锁)、以及四元数在这里不涉及因此不讲。

对于简单的第一人称相机,我们不去实现“歪头”的功能,因此只需要修改两个参数

我们用以下的方法来表示:
在这里插入图片描述
pitch 俯仰角
yaw 偏航角
roll 横滚角

于是设置三个全局变量:

float pitch = 0.0f;
float roll = 0.0f;
float yaw = 0.0f;

经过推导(引用这里先暂时懒得推导直接借用结论了

那么这两个角如何影响我们的 cameraDirection 变量,首先我们来看 xoz 平面,注意这里我们的相机是看向世界坐标 z 轴负方向的,所以 z 的计算要乘一个负号:在这里插入图片描述
然后我们来看 pitch 如何影响 y 轴分量,我们注意到 pitch 分配到 xoz 平面的分量(圆的半径)为 cos(pitch) 而 y 轴的分量为 sin(pitch)在这里插入图片描述

// 计算欧拉角以确定相机朝向
cameraDirection.x = cos(radians(pitch)) * sin(radians(yaw));
cameraDirection.y = sin(radians(pitch));
cameraDirection.z = -cos(radians(pitch)) * cos(radians(yaw)); // 相机看向z轴负方向

但是计算角度需要进行角度到弧度的转换,我们知道:
1°=π/180°
于是我们可以自己写一个角度到弧度的转换

float radians(double degree) {
	float PI = 3.1415926;
	return PI / 180 * degree;
}

此外,接下来还需要用到取模(不知道为什么不能直接用%,于是自己写了一个函数)和限制范围的函数

float clamp(float x, float min, float max)
{
	if (x > max)
		return max;
	if (x < min)
		return min;
	return x;
}
float mod(float a, float b) {
	while (a> b) {
		a = a - b;
	}
	return a;

}

在键盘交互函数中:由于四维向量不能直接相加减,所以我们在定义初始向量的时候将其定义为三维向量,方便在keyboard函数中直接对position进行加减

vec3 cameraPosition(0.0, 0.0, 2.0);
vec3 cameraDirection(0.0, 0.0, -1.0);    // 相机视线方向
vec3 cameraUp(0.0, 1.0, 0.0);
void keyboard(unsigned char key, int x, int y)
{
	
	// Todo:键盘控制相机的位置和朝向
	switch(key) 
	{
	case 033:	// ESC键 和 'q' 键退出游戏
		exit(EXIT_SUCCESS);
		break;

	case 'w':
		cameraPosition += 0.05f*cameraDirection;//为什么这里是减?因为初始时我们是面对z轴负方向的
		std::cout << "w";
		break;
	case 's':
		cameraPosition -= 0.05f*cameraDirection;
		std::cout << "s";
		break;
	case 'a':
		cameraPosition -= 0.05f*normalize(cross(cameraDirection, cameraUp));
		std::cout << "a";
		break;
	case 'd':
		cameraPosition += 0.05f*normalize(cross(cameraDirection, cameraUp));
		std::cout << "d";
		break;
	case ' ':
		cameraPosition.y += 0.05f;
		break;
	case 'c':
		cameraPosition.y -= 0.05f;
		break;
	}

	glutPostRedisplay();
}

因此传入lookAt矩阵这里也需要修改成这样:

vec4(cameraPositon,1.0)

效果:
https://www.bilibili.com/video/BV1Rz4y1r7cM/
见视频

完整代码
/*
*        Computer Graphics Course - Shenzhen University
*      Week 6 - Camera Position and Control Skeleton Code
* ============================================================
*
* - 本代码仅仅是参考代码,具体要求请参考作业说明,按照顺序逐步完成。
* - 关于配置OpenGL开发环境、编译运行,请参考第一周实验课程相关文档。
*/

#include "include/Angel.h"
#include "include/TriMesh.h"

#pragma comment(lib, "glew32.lib")

#include <cstdlib>
#include <iostream>

using namespace std;

GLuint programID;
GLuint vertexArrayID;
GLuint vertexBufferID;
GLuint vertexIndexBuffer;

GLuint vPositionID;
GLuint modelViewprojectMatrixID;

vec3 scaleTheta(1.0, 1.0, 1.0);
vec3 rotateTheta(0.0, 0.0, 0.0);
vec3 translateTheta(0.0, 0.0, 0.0);

int windowWidth = 512;  // 窗口宽
int windowHeight = 512;

TriMesh* mesh = new TriMesh();

// Viewing transformation parameters
float radius = 2.0;
float theta = 0.0;
float phi = 0.0;

float l = -1.0, r = 1.0; //left right
float b = -1.0, t = 1.0;//bottom top
float n =1.0, f = 100.0; //near far

const float dr = 5.0 * DegreesToRadians;

float pitch = 0.0f;
float roll = 0.0f;
float yaw = 0.0f;

vec3 cameraPosition(0.0, 0.0, 2.0);
vec3 cameraDirection(0.0, 0.0, -1.0);    // 相机视线方向
vec3 cameraUp(0.0, 1.0, 0.0);

vec4 eye(1.0,
	0.0,
	2.0,
	1.0);//参考课本模型

namespace Camera
{
    mat4 modelMatrix; //模型变换矩阵
    mat4 viewMatrix;  //相机观察矩阵
	mat4 projectMatrix; //投影矩阵

	mat4 ortho(const GLfloat left, const GLfloat right,
		const GLfloat bottom, const GLfloat top,
		const GLfloat zNear, const GLfloat zFar)
	{
		mat4 c;
		c[0][0] = 2.0 / (right - left);
		c[1][1] = 2.0 / (top - bottom);
		c[2][2] = 2.0 / (zNear - zFar);
		c[3][3] = 1.0;
		c[0][3] = -(right + left) / (right - left);
		c[1][3] = -(top + bottom) / (top - bottom);
		c[2][3] = -(zFar + zNear) / (zFar - zNear);
		return c;
	}

	mat4 perspective(const GLfloat fovy, const GLfloat aspect,
		const GLfloat zNear, const GLfloat zFar)
	{
		GLfloat top = tan(fovy*DegreesToRadians / 2) * zNear;
		GLfloat right = top * aspect;

		mat4 c;
		c[0][0] = zNear / right;
		c[1][1] = zNear / top;
		c[2][2] = -(zFar + zNear) / (zFar - zNear);
		c[2][3] = -2.0*zFar*zNear / (zFar - zNear);
		c[3][2] = -1.0;
		return c;
	}

	mat4 lookAt( const vec4& eye, const vec4& at, const vec4& up )
	{
		// TODO 请按照实验课内容补全相机观察矩阵的计算
		vec4 n = normalize(eye - at);
		vec3 uu = normalize(cross(up, n));
		vec4 u = vec4(uu.x, uu.y, uu.z, 0.0);
		vec3 vv = normalize(cross(n, u));
		vec4 v = vec4(vv.x, vv.y, vv.z, 0.0);
		vec4 t = vec4(0.0, 0.0, 0.0, 1.0);
		mat4 c = mat4(u, v, n, t);
		return c * Translate(-eye);
	}
}

//
// OpenGL 初始化

void init()
{
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

	// 加载shader并且获取变量的位置
	programID = InitShader("vshader.glsl", "fshader.glsl");
	vPositionID = glGetAttribLocation(programID, "vPosition");
	modelViewprojectMatrixID = glGetUniformLocation(programID, "modelViewprojectMatrix");

	// 从外部读取三维模型文件
	mesh->read_off("cube.off");

	vector<vec3f> vs = mesh->v();
	vector<vec3i> fs = mesh->f();

	// 生成VAO
	glGenVertexArrays(1, &vertexArrayID);
	glBindVertexArray(vertexArrayID);

	// 生成VBO,并绑定顶点坐标
	glGenBuffers(1, &vertexBufferID);
	glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
	glBufferData(GL_ARRAY_BUFFER, vs.size() * sizeof(vec3f), vs.data(), GL_STATIC_DRAW);

	// 生成VBO,并绑定顶点索引
	glGenBuffers(1, &vertexIndexBuffer);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vertexIndexBuffer);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, fs.size() * sizeof(vec3i), fs.data(), GL_STATIC_DRAW);

	// OpenGL相应状态设置
	glEnable(GL_LIGHTING);
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LESS);

}

//
// 渲染
float radians(double degree) {
	float PI = 3.1415926;
	return PI / 180 * degree;
}

void display()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glUseProgram(programID);

	// TODO 设置相机参数

	vec4 at(0.0, 0.0, 0.0, 1.0);//物体所在的地方
	vec4 up(0.0, 1.0, 0.0, 0.0);//观察正向向量VUP


	// 计算欧拉角以确定相机朝向

	cameraDirection.x = cos(radians(pitch)) * sin(radians(yaw));
	cameraDirection.y = sin(radians(pitch));
	cameraDirection.z = -cos(radians(pitch)) * cos(radians(yaw)); // 相机看向z轴负方向

	Camera::modelMatrix =mat4(1.0);
	Camera::viewMatrix = Camera::lookAt(vec4(cameraPosition,1.0), vec4(cameraPosition,1.0)+vec4(cameraDirection,0.0), vec4(cameraUp,1.0));//调用lookat函数来构造view矩阵
	
	Camera::projectMatrix = Camera::perspective(45.0, 1.0, 1.0, 100.0);
	//调用perspective来实现透视投影矩阵
	mat4 modelViewprojectMatrix = Camera::projectMatrix * Camera::viewMatrix * Camera::modelMatrix;
	glUniformMatrix4fv(modelViewprojectMatrixID, 1, GL_TRUE, &modelViewprojectMatrix[0][0]);

	//Camera::projectMatrix = mat4(1.0);
	//Camera::projectMatrix = Camera::ortho(l, r, b, t, n, f);
	//调用ortho函数来实现正交投影矩阵,然后将这三个矩阵按照特定的顺序相乘


	glEnableVertexAttribArray(vPositionID);
	glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
	glVertexAttribPointer(
		vPositionID,
		3,
		GL_FLOAT,
		GL_FALSE,
		0,
		(void*)0
	);

	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vertexIndexBuffer);

	glDrawElements(
		GL_TRIANGLES,
		int(mesh->f().size() * 3),
		GL_UNSIGNED_INT,
		(void*)0
	);

	glDisableVertexAttribArray(vPositionID);
	glUseProgram(0);

	glutSwapBuffers();
}

//
// 重新设置窗口

void reshape(GLsizei w, GLsizei h)
{
	glViewport(0, 0, w, h);
}

//
// 鼠标响应函数


float clamp(float x, float min, float max)
{
	if (x > max)
		return max;
	if (x < min)
		return min;
	return x;
}
float mod(float a, float b) {
	while (a> b) {
		a = a - b;
	}
	return a;

}

void mouse(int x, int y)
{
	//std::cout << x << " " << y << endl;
	yaw += 35 * (x - float(windowWidth) / 2.0) / windowWidth;
	yaw = mod(yaw + 180.0f, 360.0f) - 180.0f;    // 取模范围 -180 ~ 180
	pitch += -35 * (y - float(windowHeight) / 2.0) / windowHeight;
	pitch = clamp(pitch, -89.0f, 89.0f);
	
	glutWarpPointer(windowWidth / 2.0, windowHeight / 2.0);	// 将指针钉死在屏幕正中间

	glutPostRedisplay();
	return;
}

//
// 键盘响应函数

void keyboard(unsigned char key, int x, int y)
{
	
	// Todo:键盘控制相机的位置和朝向
	switch(key) 
	{
	case 033:	// ESC键 和 'q' 键退出游戏
		exit(EXIT_SUCCESS);
		break;
	case 'q':
		exit (EXIT_SUCCESS);
		break;
	/*case 'x': l *= 1.1; r *= 1.1; break;
	case 'X': r *= 0.9; r *= 0.9; break;
	case 'y': b *= 1.1; t *= 1.1; break;
	case 'Y': b *= 0.9; t *= 0.9; break;
	case 'z': n *= 1.1; f *= 1.1; break;
	case 'Z': n *= 0.9; f *= 0.9; break;
	case 'r': radius *= 2.0; break;
	case 'R': radius *= 0.5; break;
	case 'o': theta += dr; break;
	case 'O': theta -= dr; break;
	case 'p': phi += dr; break;
	case 'P': phi -= dr; break;

	case 't':  // 重置所有的属性为初始值
		l = -1.0;
		r = 1.0;
		b = -1.0;
		t = 1.0;
		n = -1.0;
		f = 1.0;
		radius = 1.0;
		theta = 0.0;
		phi = 0.0;
		break;*/
	case 'w':
		cameraPosition += 0.05f*cameraDirection;//为什么这里是减?因为初始时我们是面对z轴负方向的
		std::cout << "w";
		break;
	case 's':
		cameraPosition -= 0.05f*cameraDirection;
		std::cout << "s";
		break;
	case 'a':
		cameraPosition -= 0.05f*normalize(cross(cameraDirection, cameraUp));
		std::cout << "a";
		break;
	case 'd':
		cameraPosition += 0.05f*normalize(cross(cameraDirection, cameraUp));
		std::cout << "d";
		break;
	case ' ':
		cameraPosition.y += 0.05f;
		break;
	case 'c':
		cameraPosition.y -= 0.05f;
		break;

	}



	glutPostRedisplay();
}




//

void idle(void)
{
	glutPostRedisplay();
}

//

void clean()
{
	glDeleteBuffers(1, &vertexBufferID);
	glDeleteProgram(programID);
	glDeleteVertexArrays(1, &vertexArrayID);

	if (mesh) {
		delete mesh;
		mesh = NULL;
	}
}

int main(int argc, char **argv)
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
	glutInitWindowSize(500, 500);
	glutCreateWindow("OpenGL-Tutorial");

	glewInit();
	init();

	glutDisplayFunc(display);
	glutReshapeFunc(reshape);
	//glutMouseFunc(mouse);
	glutPassiveMotionFunc(mouse);

	glutKeyboardFunc(keyboard);
	glutIdleFunc(idle);

	glutMainLoop();

	clean();

	return 0;
}

实验3.2 投影和阴影

一、 实验目的

  1. 了解OpenGL中正交投影和透视投影变换
  2. 了解在OpenGL中实现正交投影和透视投影变换
  3. 了解使用投影变换实现场景的硬阴影效果

二、 理论背景

  1. 投影变换
    在经过三维物体的模-视变换后,场景中的三维物体即被放在了相机能够观察到的位置。

而投影变换的目的则是定义一个视景体(View Volume),使得视景体外多余的部分被裁减掉,最终进入到投影平面上的只是视景体内的部分。

裁剪应该可以类比为人类的视野其实也是有限的,因此在计算机中只需要将视景体内的物体保留下来即可。

投影包含正交投影(Orthographic Projection)和透视投影(Perspective Project)两种。
可以通过看图来快速理解:
正交投影:
在这里插入图片描述

正交投影的效果现实中是不存在的,现实中都是透视投影

透视投影:
在这里插入图片描述
透视投影效果更接近现实
接下来我们细讲

正交投影

正交投影的投影线垂直于观察平面,如果用照相机实现正交投影,那么这个照相机的胶片平面应该平行于镜头。如下图所示,投影平面是 。
在这里插入图片描述
在OpenGL中,通常使用的正交投影是定义在一个平行六面体的视景体(或者说是裁剪体)中,如下图所示,该六面体由六个参数决定,分别为左右裁剪平面(left和right),上下裁剪平面(top和bottom),远近裁剪平面(near和far)。需要注意的是,这些参数的定义都是在相机坐标系下。举例来说,远近裁剪平面相当于是在z轴方向离相机的距离。

在这里插入图片描述
但是,在OpenGL的渲染管线中定义了一个标准视景体如下,

也就是说,在渲染的最后过程中,我们需要将上述定义的正交投影视景体变换到该标准视景体中,使用的方法是通过平移和旋转变换将相机坐标系下经过裁剪的顶点变换到默认的标准视景体下,这个处理过程称为投影规范化(ProjectionNormalization),如下图所示。

在这里插入图片描述
而使用的投影矩阵如下,具体推导过程请参考书本相应章节。

透视投影

在真实世界中的投影方式是透视投影,即满足人类视觉基本感知原理:近大远小。如下图所示,将坐标原点作为投影中心,任意顶点 经过投影平面 之后均相交于坐标原点,那么基于比例关系即可求出在投影平面上的投影点坐标。
在这里插入图片描述

这里都是实验手册里说的,你看到这里的时候可能无法完全理解(看完了跟没看一样 ),我也是,现在来细说一下这一块:

透视除法

如果你曾经体验过实际生活给你带来的景象,你就会注意到离你越远的东西看起来更小。这个神奇的效果我们称之为透视(Perspective)。透视的效果在我们看一条无限长的高速公路或铁路时尤其明显,由于透视的原因,平行线似乎在很远的地方看起来会相交。这正是透视投影(Perspective Projection)想要模仿的效果,它是使用透视投影矩阵来完成的。

首先思考正交投影和透视投影的区别,例如看张图的前后两根柱子,在正交投影中,我们可以建立坐标系
在这里插入图片描述
如图所示我们可以看到,投影其实就是将三维场景投影到一个二维平面,在正交投影中,我们可以看到1-6号柱子的x、y坐标是完全一样的,那么投影方式是平行的,那么第一根柱子就会完全挡住2-6号柱子,此时就丧失了深度信息

那么如果我们想将深度信息保留下来,需要做的就是实现近大远小的功能。

如果实现?

我们再来看一次透视投影的效果:
在这里插入图片描述

我们可以看到,为什么透视投影效果看起来真实很多,就是因为它体现了深度,具体是怎么体现的呢?

我们可以看到最后一根柱子也就是6号柱子,它在投影平面内的x坐标明显和1号柱子的x坐标不同。

越远的柱子,它的x坐标越小。

所以也就是说,对于z越大的物体,它经过投影后的x坐标应该越小!

此处借用知乎大佬的推导:

在这里插入图片描述
在图形学中表示:
在这里插入图片描述
经过与上面类似的相似三角形的推导,不难得出:

在这里插入图片描述
于是可以得到投影后的齐次坐标:
在这里插入图片描述

当然除了上面那种推导方式,还可以来体会另外一种方式:
我们知道齐次坐标的最后一个信息(也就是w)如果是1则代表坐标值,此时我们可以用这个位置来储存深度信息:
所以可以构造一个投影矩阵:
在这里插入图片描述
再将其乘以坐标:
在这里插入图片描述
此时这个坐标的深度信息就被保留了下来,但是在齐次坐标中最后一个的取值只有0和1,如果z/d不为1,我们就需要变换。
在这里插入图片描述
因此,最终就得到了透视投影的坐标。

投影矩阵的建立

同样,对于透视投影,我们也需要设置一个视景体来裁剪三维物体如下,也就是说在视景体内部的三维物体才能被投影到投影平面上,剩下超出的部分则被裁减掉。在透视投影中,由于投影中心在理想情况下是一个点,所以形成的视景体构成一个截头椎体(Frustum)。

在这里插入图片描述
在OpenGL中,通常有两种定义透视投影视见体的方法,如下左图的类似于正交投影的棱台视见体和下右图利用视域(FoV,Field of View)定义的视见体。其中棱台视见体是由左右裁剪平面(left和right),上下裁剪平面(top和bottom),远近裁剪平面(near和far)。而视域视见体则由视角(Field of View),投影平面长宽比(aspect)和远近裁剪平面(near和far)决定。
在这里插入图片描述

这个图和人类看世界的感觉是一样的,都是从一个点往外看

另外,在透视投影中,同样需要执行投影规范化过程,如下图所示,将视景体内部的顶点变换到标准视景体范围内:
在这里插入图片描述
以透视投影的棱台视见体为例,其透视投影矩阵如下,具体推导过程请参考书本相应章节,这里不再赘述。(请尝试推导出视域视景体的投影变换矩阵,并理解这两种视景体定义方式的等价性。)
在这里插入图片描述
(注意和教材上第166页公式不一样,第三行第三列数值教材上的写错了。)

当然像我这种懒狗是不会去推导的,推导是不可能推导的,只有调用函数才能维持得了生活这样
透视投影函数有两个,perspective和frustum,这里主要展示perspective:


	mat4 perspective( const GLfloat fovy, const GLfloat aspect,
		const GLfloat zNear, const GLfloat zFar)
	{
		// TODO 请按照实验课内容补全相机观察矩阵的计算
		GLfloat top = tan(fovy * M_PI / 180 / 2) * zNear;
		GLfloat right = top * aspect;

		mat4 c;
		c[0][0] = zNear / right;
		c[1][1] = zNear / top;
		c[2][2] = -(zFar + zNear) / (zFar - zNear);
		c[2][3] = -(2.0*zFar*zNear) / (zFar - zNear);
		c[3][2] = -1.0;
		c[3][3] = 0.0;
		return c;
	}

投影和硬阴影

[注意]本节内容和书本上通过模-视变换矩阵来计算阴影投影矩阵方法不一样。
投影变换矩阵的一个应用就是生成简单的阴影。从物理上看,有光源才会产生阴影,如果光源投射的光线被物体遮挡,那么遮挡的部分被投影在地面上即产生了阴影。为了简单起见,我们假定阴影落在了地面上,即OpenGL坐标系下的 平面。
如下图所示,我们假定光源位置在 ,物体由三角形表示,投影平面上的黑色三角形区域即为阴影,我们称之为阴影多边形(Shadow Polygon)。
在这里插入图片描述
下面我们来推导阴影投影矩阵如下。假设三角形任意一个顶点坐标为 ,投影到投影平面之后的坐标为 ,因为该点在 平面上,所以 。根据比例关系可得如下公式:
在这里插入图片描述
求解可得,
在这里插入图片描述在这里插入图片描述
为了能够方便的通过矩阵表示出投影关系,我们将所有坐标设置在齐次坐标系下,那么投影关系就能表示成如下公式:
即可得到硬阴影对应的投影矩阵:
最后通过透视除法得到最终的投影坐标:
在这里插入图片描述
所以生成硬阴影的关键在于投影矩阵的求解。

三、 实验内容

1. 正交投影和透视投影的实现

本实验主要目标是实现正交投影矩阵生成函数
mat4 ortho( const GLfloat left, const GLfloat right,
const GLfloat bottom, const GLfloat top,
const GLfloat zNear, const GLfloat zFar )
和透视投影生成矩阵
mat4 perspective( const GLfloat fovy, const GLfloat aspect,
const GLfloat zNear, const GLfloat zFar)
a) 按照实验1.1配置环境,具体请参考实验1.1相关文档。注意:会编译出错,因为缺少文件“TriMesh.h”,请在Blackboard找实验3.1的压缩包下载后,将其拷贝到你的include文件夹中。后续实验都会用到该文件,请将其保留在你的include文件夹中。
b) 读入立方体文件
c) 根据上述理论说明部分,实现正交投影和透视投影生成矩阵函数。
d) 将生成的矩阵传入顶点着色器,并观察结果。
// init():从顶点着色器中读取投影变换矩阵的位置
modelViewProjMatrixID = glGetUniformLocation(programID, “modelViewProjMatrix”);

// display():计算观察矩阵和投影变换矩阵并传入顶点着色器
vec4 eye( radius * sin(theta) * cos(phi),
      radius * sin(theta) * sin(phi),
      radius * cos(theta),
      1.0);
	vec4 at(0.0, 0.0, 0.0, 1.0);
	vec4 up(0.0, 1.0, 0.0, 0.0);

	Camera::modelMatrix = mat4(1.0);

Camera::viewMatrix = Camera::lookAt(eye, at, up);
Camera::projMatrix = Camera::ortho/perspective。。。
mat4 modelViewProjMatrix = Camera::projMatrix * Camera::viewMatrix * Camera::modelMatrix;

glUniformMatrix4fv(modelViewProjMatrixID, 1, GL_TRUE, &modelViewProjMatrix[0][0]);

e) 通过键盘控制,完成对不同参数的控制。具体控制可参考教材P157-158页代码。

投影和硬阴影的实现

请按照如下说明顺序阅读代码并完成实验。
a) 绘制三角形。参考实验1,配置开发环境并画出平面三角形。然后修改三角形顶点坐标为:(-0.5,0.5,0.5),(0.5,0.5,0.5)和(0.0,0.75,0.0),绘制得到一个空间三角形。并使用glClearColor函数将窗口背景色改成(0.9,0.9,0.9,0.0),因为之后我们将阴影设置为黑色,所以这里将窗口背景改成其他颜色,以突出阴影颜色的显示。代码说明如下:

// init():设置窗口背景颜色
glClearColor(0.9, 0.9, 0.9, 0.0);

// init():生成顶点坐标
vec3 points[3];
points[0] = vec3(-0.5, 0.5, 0.5);
points[1] = vec3(0.5, 0.5, 0.5);
points[2] = vec3(0.0, 0.75, 0.0);
// init():从顶点着色器中获取顶点坐标位置
vPositionID = glGetAttribLocation(programID, "vPosition");

// init():生成缓存将顶点坐标传入着色器中
glGenBuffers(1, &vertexBufferID);
glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
glBufferData(GL_ARRAY_BUFFER, sizeof(points), points, GL_STATIC_DRAW);

b) 调整相机视角。因为本节课重点不在观察和投影,所以简单的将模型变换矩阵和相机观察矩阵设置为默认参数,并将投影矩阵设置为正交投影,调用实验1:投影变换中实现的ortho函数将参数分别设置为左右裁剪平面-3和+3,上下裁剪平面-3和+3,远近裁剪平面-3和+3(请思考这些参数的意义是什么。)。将这些矩阵在程序中计算后传入顶点着色器中。代码说明如下:
// init():从顶点着色器中获取变换矩阵位置
modelMatrixID = glGetUniformLocation(programID, “modelMatrix”);
viewMatrixID = glGetUniformLocation(programID, “viewMatrix”);
projMatrixID = glGetUniformLocation(programID, “projMatrix”);

// display():设置模型视图矩阵和观察矩阵
// 将投影矩阵设置为正交投影,参数为左右平面-3和3,上下平面-3和3,远近-3和3。

// 由于使用默认相机参数时,相机看的方向与投影平面平行,因此,调整一下相机的方向和位置
Camera::modelMatrix = mat4(1.0);
vec4 eye( 0.5, 2.0, -0.5, 1.0); //
vec4 at( 0, 0, 0, 1); // 原点
vec4 up( 0, 1, 0, 0); // 默认方向
Camera::viewMatrix = Camera::lookAt(eye, at, up);
Camera::modelMatrix = mat4(1.0);
Camera::projMatrix = Camera::ortho(-3, 3, -3, 3, -3, 3);

// display():计算变换矩阵并将结果传入顶点着色器中
glUniformMatrix4fv(modelMatrixID, 1, GL_TRUE, &Camera::modelMatrix[0][0]);	glUniformMatrix4fv(viewMatrixID, 1, GL_TRUE, &Camera::viewMatrix[0][0]);
glUniformMatrix4fv(projMatrixID, 1, GL_TRUE, &Camera::projMatrix[0][0]);

c) 计算阴影投影矩阵。我们定义一个全局变量lightPos表示光源位置,在参考程序中设置为(-0.5,2.0,0.5),继而参考上述理论说明部分计算阴影投影矩阵,更新模型视图矩阵,并将该矩阵传入到顶点着色器中,以执行对原始三角形顶点的投影变换。代码说明如下:(请思考在模型视图矩阵的更新方式)

// 全局变量:设置光源位置
float lightPos[3] = { -0.5, 2.0, 0.5 };

// display():按照公式计算投影矩阵
float lx = lightPos[0];
float ly = lightPos[1];
float lz = lightPos[2];

mat4 shadowProjMatrix(-ly, 0.0, 0.0, 0.0,
	lx, 0.0, lz, 1.0,
	0.0, 0.0, -ly, 0.0,
	0.0, 0.0, 0.0, -ly);

// display():将投影矩阵传入顶点着色器中的模-视变换矩阵

Camera::modelMatrix = shadowProjMatrix
glUniformMatrix4fv(modelMatrixID, 1, GL_TRUE, &Camera::modelMatrix[0][0]);

d) 投影三角形的绘制。在绘制过程中,我们可以简单地将三角形绘制两次即可。第一次是按照常规方式绘制,第二次是使用了阴影变换矩阵之后对新的阴影三角形进行绘制。也就是说,第一次绘制时,将阴影投影矩阵设置为单位矩阵,而第二次绘制是计算出矩阵值之后变换三角形得到投影三角形。请参考代码中的display函数,这里需要将上述各部分进行整合,而得到最终的绘制结果。
e) 键盘控制阴影的显示。观察当物体(三角形)绕着Y轴旋转时,投影的变化效果(可通过修改modelMatrix来实现)。请参考代码中的key函数,实现对应的键盘控制。
f) 请仔细读顶点着色器的内容(vshader.glsl),并思考原因。

四、 示例和练习

a) 完成上述实验内容并实现对任意简单几何体的阴影变换。
b) 键盘控制三角形的旋转。
c) 在上述实验内容的基础上,实现采用鼠标控制光源位置,并在鼠标点击之后计算阴影投影矩阵和绘制阴影多边形。按照上述步骤完成之后会得到如下图所示的结果,通过Y键能够观察旋转的三角形(红色)和投影生成的阴影(黑色)。

在这里插入图片描述
正交投影和透视投影变换:
相比3.1只需要修改两个地方,因此我直接以3.1为模型进行修改:
在camera里添加以下两个函数:

添加两个函数: ortho正交投影函数
在这里插入图片描述

和perspective透视投影函数
在这里插入图片描述
在这里插入图片描述
如此便可得到实现平行投影的正方体,效果如图:
在这里插入图片描述

那么接下来要实现透视投影,只需要按照下图来实现即可:
在这里插入图片描述
在这里插入图片描述

硬阴影

阴影的实现有两种办法,一种方法是使用两个着色器,传入两次矩阵。另外一直方法是在着色器里添加一个bool变量,传入物体的位置矩阵的时候,判断需要着色的物体还是阴影。
(在本次实验中,由于物体的阴影比较简单,直接传入绘制即可)
在display函数中绘制完物体后,修改传入着色器的model矩阵和颜色,然后再调用glDrawArray绘制即可。

float lx = lightPos[0];
	float ly = lightPos[1];
	float lz = lightPos[2];

	mat4 shadowProjMatrix(-ly, 0.0, 0.0, 0.0,
		lx, 0.0, lz, 1.0,
		0.0, 0.0, -ly, 0.0,
		0.0, 0.0, 0.0, -ly);//阴影矩阵

	Camera::modelMatrix = shadowProjMatrix * Camera::modelMatrix;

	glUniform4fv(fColorID, 1, black);
	glDrawArrays(GL_TRIANGLES, 0, 3);

键盘交互:
在keyboard函数中修改自己定义的rotationAngle变量,

void keyboard(unsigned char key, int x, int y)
{
	switch (key)
	{
	case 033:
		exit(EXIT_SUCCESS);
		break;
	case 'q':
		exit(EXIT_SUCCESS);
		break;
	case 'y':		// 'y' 键使得三角形旋转-1.0度
		rotationAngle -= 1.0;
		break;
	case 'Y':		// 'Y' 键使得三角形旋转+1.0度
		rotationAngle += 1.0;
		break;
	}
	glutPostRedisplay();
}

然后修改物体的model矩阵,让物体的model矩阵乘以RotateY函数的矩阵即可

	Camera::modelMatrix = mat4(1.0)* RotateY(rotationAngle);

完整代码:

#include "Angel.h"

#pragma comment(lib, "glew32.lib")

#include <cstdlib>
#include <iostream>

using namespace std;

GLuint programID;
GLuint vertexArrayID;
GLuint vertexBufferID;
GLuint vertexIndexBuffer;

GLuint vPositionID;
GLuint rotationMatrixID;
GLuint modelMatrixID;
GLuint viewMatrixID;
GLuint projMatrixID;
GLuint fColorID;

// 相机视角参数
float l = -3.0, r = 3.0;    // 左右裁剪平面
float b = -3.0, t = 3.0;    // 上下裁剪平面
float n = -3.0, f = 3.0;    // 远近裁剪平面
float rotationAngle = 0.0; // 旋转角度

vec4 red(1.0, 0.0, 0.0, 1.0);
vec4 black(0.0, 0.0, 0.0, 1.0);

float lightPos[3] = { -0.5, 2.0, 0.5 };



//
// 相机参数控制

namespace Camera
{
	mat4 modelMatrix;
	mat4 viewMatrix;
	mat4 projMatrix;

	mat4 ortho(const GLfloat left, const GLfloat right,
		const GLfloat bottom, const GLfloat top,
		const GLfloat zNear, const GLfloat zFar)
	{
		// TODO 请按照实验课讲解补全正交投影矩阵的计算
		mat4 c;
		c[0][0] = 2.0 / (right - left);
		c[1][1] = 2.0 / (top - bottom);
		c[2][2] = 2.0 / (zNear - zFar);
		c[3][3] = 1.0;
		c[0][3] = -(right + left) / (right - left);
		c[1][3] = -(top + bottom) / (top - bottom);
		c[2][3] = -(zFar + zNear) / (zFar - zNear);
		return c;
	}


	mat4 lookAt(const vec4& eye, const vec4& at, const vec4& up)
	{
		// TODO 请按照实验课内容补全相机观察矩阵的计算
		vec4 n = normalize(eye - at);
		vec3 uu = normalize(cross(up, n));
		vec4 u = vec4(uu.x, uu.y, uu.z, 0.0);
		vec3 vv = normalize(cross(n, u));
		vec4 v = vec4(vv.x, vv.y, vv.z, 0.0);
		vec4 t = vec4(0.0, 0.0, 0.0, 1.0);
		mat4 c = mat4(u, v, n, t);
		return c * Translate(-eye);
	}
}

//
// OpenGL 初始化

void init()
{
	// 设置窗口背景颜色
	glClearColor(0.9f, 0.9f, 0.9f, 0.0f);
	programID = InitShader("vshader.glsl", "fshader.glsl");

	// init():生成顶点坐标
	vec3 points[3];
	points[0] = vec3(-0.5, 0.5, 0.5);
	points[1] = vec3(0.5, 0.5, 0.5);
	points[2] = vec3(0.0, 0.75, 0.0);

	// 从顶点着色器中获取相应变量的位置
	vPositionID = glGetAttribLocation(programID, "vPosition");
	//rotationMatrixID = glGetUniformLocation(programID, "rotationMatrix");
	modelMatrixID = glGetUniformLocation(programID, "modelMatrix");
	viewMatrixID = glGetUniformLocation(programID, "viewMatrix");
	projMatrixID = glGetUniformLocation(programID, "projMatrix");
	fColorID = glGetUniformLocation(programID, "fColor");

	// 生成VAO
	glGenVertexArrays(1, &vertexArrayID);
	glBindVertexArray(vertexArrayID);

	// 生成VBO,并绑定顶点坐标
	glGenBuffers(1, &vertexBufferID);
	glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
	glBufferData(GL_ARRAY_BUFFER, sizeof(points), points, GL_STATIC_DRAW);

	// OpenGL相应状态设置
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LESS);


}

//
// 渲染

void display()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glUseProgram(programID);

	// TODO 设置模-视变换矩阵,投影矩阵
	// 由于使用默认相机参数时,相机看的方向与投影平面平行,因此,调整一下相机的方向和位置
	vec4 eye(0.5, 2.0, -0.5, 1.0); // 光源关于y-z平面的对称方向
	vec4 at(0, 0, 0, 1);   // 原点
	vec4 up(0, 1, 0, 0);      // 默认方向

	// (因为本节重点不在于投影变换,所以将投影矩阵设置为正交投影即可)
	Camera::viewMatrix = Camera::lookAt(eye, at, up);
	//用lookAt函数来使得相机朝向视点
	Camera::modelMatrix = mat4(1.0)* RotateY(rotationAngle);
	//乘上rotate使得模型矩阵可以旋转
	Camera::projMatrix = Camera::ortho(l, r, b, t, n, f);
	//正交投影的方法来实现投影

	//mat4 rotationMatrix = RotateX(rotationAngle);
	//glUniformMatrix4fv(rotationMatrixID, 1, GL_TRUE, &rotationMatrix[0][0]);


	glEnableVertexAttribArray(vPositionID);
	glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
	glVertexAttribPointer(
		vPositionID,
		3,
		GL_FLOAT,
		GL_FALSE,
		0,
		(void*)0
	);

	// TODO 在正常的投影矩阵下绘制原始的三角形(用红色表示)


	glUniformMatrix4fv(viewMatrixID, 1, GL_TRUE, &Camera::viewMatrix[0][0]);
	glUniformMatrix4fv(projMatrixID, 1, GL_TRUE, &Camera::projMatrix[0][0]);
	glUniformMatrix4fv(modelMatrixID, 1, GL_TRUE, &Camera::modelMatrix[0][0]);

	glUniform4fv(fColorID, 1, red);
	glDrawArrays(GL_TRIANGLES, 0, 3);

	// TODO 计算阴影投影矩阵,绘制投影之后的三角形(用黑色表示)
	// mat4 shadowProjMatrix = ...;

	float lx = lightPos[0];
	float ly = lightPos[1];
	float lz = lightPos[2];

	mat4 shadowProjMatrix(-ly, 0.0, 0.0, 0.0,
		lx, 0.0, lz, 1.0,
		0.0, 0.0, -ly, 0.0,
		0.0, 0.0, 0.0, -ly);//阴影矩阵

	Camera::modelMatrix = shadowProjMatrix * Camera::modelMatrix;
	//给阴影矩阵也乘上一个旋转矩阵,使得其可以跟随上面的三角形一样一起旋转
	//glUniformMatrix4fv(modelMatrixID, 1, GL_TRUE, &Camera::modelMatrix[0][0]);


	// 请阅读顶点着色器的内容,对照实验1的异同
	//glUniformMatrix4fv(viewMatrixID, 1, GL_TRUE, &Camera::viewMatrix[0][0]);
	//glUniformMatrix4fv(projMatrixID, 1, GL_TRUE, &Camera::projMatrix[0][0]);
	glUniformMatrix4fv(modelMatrixID, 1, GL_TRUE, &Camera::modelMatrix[0][0]);
	//将留空代码中的注释打开,使得能正常绘制阴影矩阵

	//Camera::modelMatrix = shadowProjMatrix;
	//glUniformMatrix4fv(modelMatrixID, 1, GL_TRUE, &Camera::modelMatrix[0][0]);

	glUniform4fv(fColorID, 1, black);
	glDrawArrays(GL_TRIANGLES, 0, 3);

	glDisableVertexAttribArray(vPositionID);
	glUseProgram(0);

	glutSwapBuffers();
}

//
// 重新设置窗口

void reshape(GLsizei w, GLsizei h)
{
	glViewport(0, 0, w, h);
}

//
// 鼠标响应函数

void mouse(int button, int state, int x, int y)
{
	if (state == GLUT_LEFT_BUTTON && state == GLUT_DOWN) {
		lightPos[0] = x;//将光源的xy坐标设置为鼠标左键按下的x、y的坐标
		lightPos[1] = y;
	}
	return;
}

//
// 键盘响应函数

void keyboard(unsigned char key, int x, int y)
{
	switch (key)
	{
	case 033:
		exit(EXIT_SUCCESS);
		break;
	case 'q':
		exit(EXIT_SUCCESS);
		break;
	case 'y':		// 'y' 键使得三角形旋转-1.0度
		rotationAngle -= 1.0;
		break;
	case 'Y':		// 'Y' 键使得三角形旋转+1.0度
		rotationAngle += 1.0;
		break;
	}
	glutPostRedisplay();
}

void printHelp()
{
	printf("%s\n\n", "Camera");
	printf("Keyboard options:\n");
	printf("y: Decrease rotation angle (aroud Y axis)\n");
	printf("Y: Increase rotation angle (aroud Y axis)\n");
}

//

void idle(void)
{
	glutPostRedisplay();
}

//

void clean()
{
	glDeleteBuffers(1, &vertexBufferID);
	glDeleteProgram(programID);
	glDeleteVertexArrays(1, &vertexArrayID);
}

//

int main(int argc, char **argv)
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
	glutInitWindowSize(500, 500);
	glutCreateWindow("OpenGL-Tutorial");

	glewInit();
	init();

	glutDisplayFunc(display);
	glutReshapeFunc(reshape);
	glutMouseFunc(mouse);
	glutKeyboardFunc(keyboard);
	glutIdleFunc(idle);

	printHelp();
	glutMainLoop();

	clean();

	return 0;
}

实验3.3 Phong反射模型(1)

一、 实验目的

  1. 了解OpenGL中基本的光照模型
  2. 掌握OpenGL中实现基于顶点的光照计算

二、 理论背景

通过仔细观察前面章节中所有绘制的例子可以发现,所得到的图像基本上是平的,没有深度感,从而不能显示出模型的三维特性,不是很符合真实场景中人类视觉对三维物体的感知。这是因为我们假设物体表面只有单独一种颜色,在这样的假设下,球面的正投影将是均匀着色的圆。类比一下真实场景,我们能看到物体具体不同的颜色是因为眼睛接收了特定波长的光,不透明物体的颜色取决于它反射的色光,而透明物体的颜色则取决于透过它的色光。简单来说,物体只有在光照条件下才能呈现出不同的颜色,并且由于光照可以产生不均匀着色和明暗度的效果。在本周我们将考虑简单的光源模型和几种描述光线-材质之间相互作用的模型,目的是在图形绘制流水线中增加明暗处理功能。在现代OpenGL绘制管线中,我们可以将光照模型加在顶点着色器中,或者在片元着色器中。
光照模型包括局部光照和全局光照。局部光照指物体表面上一点的颜色只取决于表面的材质属性、表面的局部集合性质以及光源的位置和属性,而与场景中其他的表面无关。而全局光照则需要考虑场景中所有表面和光源相互作用的照射效果。如下图所示,左图中x点接收到周围环境的光线照射表示全局光照,右图中点x接收来自光源的直接照射表示局部光照。在本周我们只考虑局部光照。
在这里插入图片描述
在局部光照模型中,我们考虑从光源发出的光线,并对这些光线和场景中反射光线的表面之间的相互作用进行建模。这个问题包括两个独立的部分,首先是对场景中的光源建模,其次是必须构建一个描述物体表面和光线之间相互作用的模型。本周我们主要讨论Phong反射模型,它是对基于物理模型绘制方法的一种近似,并且具有很高的效率,能支持各种各样的光照条件和材质属性。
Phong反射模型中涉及到了4个基本向量,如下图所示。其中p为三维物体表面上的一点,l是从点p指向光源位置的向量,n表示p点的法向量,v是从p点指向相机(观察者)的向量,r是沿着l方向入射光线按照反射定于的出射方向。注意,在下面的计算中,我们假定所有的向量都已经归一化。
在这里插入图片描述
Phong反射模型考虑了光线和材质之间的三种相互作用:环境光反射、漫反射和镜面反射,最终在三维物体表面上的每个点的颜色由这三个成分组合表示如下。
在这里插入图片描述
环境光反射 :在物体表面所有点处的环境光强度都是相同的。环境光一部分被表面吸收,一部分被表面反射,被反射部分由环境光反射系数决定,所以最终得到的环境光部分可以由如下公式得到:
在这里插入图片描述
其中 可以表示任何单独的光源,也可以代表全局环境光。
在实际场景中,当一个光源发出的一部分光线直接照射到物体表面的时候,通常对最终渲染图像有贡献的部分大可通过漫反射和镜面发射进行建模而近似表示,但是考虑场景的复杂性,通常光线会被场景中其他物体反射多次后重新射入我们正在考虑的这个表面上,为了降低计算的复杂性同时又更加逼真的表示场景的光照效果,我们通过加入环境光分量来近似这部分贡献。
漫反射 :理想的漫反射表面会把光线向所有方向均匀散射,这样的表面称之为Lambert表面(Lambertian Surface),根据Lambert定律,只有入射光线的垂直分量才对照明起到作用,也就是说漫反射部分的光照效果与法向量n和光源向量l的夹角大小有关。如下图所示。同时考虑漫反射系数kd表示表面对漫反射光的反射程度,最终结果如下所示:
在这里插入图片描述
在这里插入图片描述
其中 表示漫反射光线的强度。另外需要注意的地方,如果光源位于物体表面以下,那么上述公式中 值将取负数,但在实际情况下这时物体表面并没有受到漫反射光的照射,所以当取负数时应该直接取0而消除此时漫反射光对最终结果的影响。此外,从物理角度来看,光照的强度会随着光源与物体间距离变长而逐渐衰减,综合考虑上述因素得到漫反射成分的计算公式如下:
在这里插入图片描述
其中d表示光源和三维物体表面上点的距离,a,b和c表示距离衰减系数。
镜面反射 :在漫反射光照模型中,我们假设了物体表面是均匀粗糙的。而镜面反射成分是为了模拟光滑表面,而且表面越光滑,反射出去的光线越集中在一个角度附近,越接近真实的镜子(不透明镜子),如下图所示。
在这里插入图片描述
Phong提出了一个近似模型考虑镜面反射光部分,将表面看成是光滑的,观察者看到的光线强度取决于物体表面反射光的方向r和观察方向v这两者之间的夹角,用公式表示如下。
在这里插入图片描述
类似于漫反射,我们可以像计算漫反射分量那样加上一个距离因子。
综合上述几种不同的光照情况,我们得到最终的Phong模型公式如下。
在这里插入图片描述

三、 实验内容

请按照如下说明顺序阅读代码并完成实验。
需要说明的是,在OpenGL绘制流水线中,光照计算需要等到三维物体经过相机变换和投影变换之后才能进行,因为此时物体在三维空间中的位置才完全确定下来。另外,通常情况下,我们在相机坐标系下来计算所有向量。具体实验过程如下。
a) 参考实验2.2,读入球模型并绘制。另外,关于OpenGL开发环境配置,详细请参考实验1.1。
b) 在main.cpp程序中计算球面上每个顶点的法向量,并传入顶点着色器。假设球心位于 ,球面上顶点坐标为 ,那么该点的法向量为 ,注意计算完成后对法向量进行归一化。在程序的初始化阶段,为法向量创建缓存,并将数据传入到顶点着色器如下。
// 获取三维物体的顶点坐标
vector vs = mesh->v();
vector ns;

// 计算球面上每个顶点的法向量并归一化,默认球心位于坐标系原点
for (int i = 0; i < vs.size(); ++i) {
	ns.push_back(vs[i] - vec3(0.0, 0.0, 0.0));
}

// 从顶点着色器中获取法向量位置,并传入数值
glGenBuffers(1, &vertexNormalID);
glBindBuffer(GL_ARRAY_BUFFER, vertexNormalID);
glBufferData(GL_ARRAY_BUFFER, ns.size() * sizeof(vec3f), ns.data(), GL_STATIC_DRAW);

c) 在main.cpp程序中的display函数中,计算相机观察矩阵和投影矩阵,并将矩阵和相机位置传入顶点着色器。
// 计算相机观察矩阵和投影矩阵,并传入顶点着色器
mat4 modelViewMatrix = Camera::viewMatrix * Camera::modelMatrix;
mat4 modelViewProjMatrix = Camera::projMatrix * modelViewMatrix;

// TODO 将相机位置传入顶点着色器

// glUniformMatrix4fv(modelViewMatrixID, 1, GL_TRUE, …);
// glUniformMatrix4fv(modelViewProjMatrixID, 1, GL_TRUE, …);
// 将相机位置传入顶点着色器
// glUniform3fv(lightPosID, 1, …);
d) 在顶点着色器中将顶点坐标和法向量变换到相机坐标系下。
vec4 vertPos_cameraspace = modelViewMatrix * vec4(vPosition, 1.0);
vec3 V = vertPos_cameraspace.xyz / vertPos_cameraspace.w;
vec3 N = (modelViewMatrix * vec4(vNormal, 0.0)).xyz;
需要注意的是,在变换得到相机坐标系下的顶点位置时,需要执行透视除法将其坐标变换到正常坐标系下。
e) 在顶点着色器中执行为每个顶点执行光照计算。
首先在着色器中定义环境光反射系数、漫反射系数和镜面反射系数的每个颜色分量(ka,kd,ks)如下,
vec3 ambiColor = vec3(0.2, 0.2, 0.2);
vec3 diffColor = vec3(0.5, 0.5, 0.5);
vec3 specColor = vec3(0.3, 0.3, 0.3);
在本例中,光源的La,Ld,Ls,均为白光,即,光源所包含的环境光,漫反射光,和镜面反射光均为(1.0,1.0,1.0)的白光。
然后计算Phong反射模型涉及到的四个向量,并归一化,其中使用的normalize函数,reflect函数均为GLSL语言内置函数,分别是归一化向量和依据入射向量和法向量计算反射向量。如下所示:
vec3 N_norm = normalize(N);
vec3 L_norm = normalize(lightPos - V);
vec3 V_norm = normalize(-V);
vec3 R_norm = reflect(-L_norm, N_norm);
依据上述公式,计算漫反射分量和镜面反射分量如下,注意代码中使用了GLSL语言内置函数clamp,主要作用是将函数值裁剪到规定的两个值之间,具体使用方法请参考相关书籍。
float lambertian = clamp(dot(L_norm, N_norm), 0.0, 1.0);
float specular = clamp(dot(R_norm, V_norm), 0.0, 1.0);
最后为累加三个部分的颜色分量,得到每个顶点的颜色如下:
color = vec4(ambiColor +
diffColor * lambertian +
specColor * pow(specular, 10.0), 1.0);
f) 在片元着色器中输出颜色。
fragmentColor = color;

四、 示例和练习

a) 在顶点着色器中,分别只用环境光反射分量、漫反射分量和镜面反射分量作为最后的光照结果,观察输出。
b) 上述代码示例中的实现的光照模型并没有考虑到光源和物体表面顶点之间距离的影响,请尝试加入这一项。
c) 参考教材第189页,在着色器中实现改进的Phong反射模型(即Blinn-Phong反射模型)。
d) 加入鼠标控制,用鼠标控制光源位置,实现不同光源位置下的光照效果。

按照上述实验步骤,读入sphere_coarse.off模型,得到的光照效果如下图所示。
在这里插入图片描述
首先在init函数中读入球的模型,并计算计算球模型在每个顶点的法向量,并存储到ns数组中。
在这里插入图片描述

然后从顶点着色器中获取法向量位置,并传入数值
在这里插入图片描述
然后在display函数中,参照3.2的方法计算相机观察矩阵和投影矩阵,并将矩阵和相机位置传入顶点着色器。

在这里插入图片描述
在这里插入图片描述
在顶点着色器中将顶点坐标和法向量变换到相机坐标系下。
在这里插入图片描述
需要注意的是,在变换得到相机坐标系下的顶点位置时,需要执行透视除法将其坐标变换到正常坐标系下。
然后计算Phong反射模型涉及到的四个向量,并归一化,其中使用的normalize函数,reflect函数均为GLSL语言内置函数,分别是归一化向量和依据入射向量和法向量计算反射向量。如下所示:

在这里插入图片描述
依据上述公式,计算漫反射分量和镜面反射分量如下,注意代码中使用了GLSL语言内置函数clamp,主要作用是将函数值裁剪到规定的两个值之间,具体使用方法请参考相关书籍。
在这里插入图片描述
最后为累加三个部分的颜色分量,得到每个顶点的颜色如下:
在这里插入图片描述
代码写完后我们运行可以得到如图所示的结果:
在这里插入图片描述
二、 示例和练习
a) 在顶点着色器中,分别只用环境光反射分量、漫反射分量和镜面反射分量作为最后的光照结果,观察输出。
如果只用环境光,则只需要在顶点着色器进行如图所示的操作:
在这里插入图片描述
在这里插入图片描述
剩下的同理。

b) 上述代码示例中的实现的光照模型并没有考虑到光源和物体表面顶点之间距离的影响,请尝试加入这一项。
当我们考虑加入距离情况时,则需要对漫反射和镜面反射光进行修改,不需要对环境光进行修改。
我们参照漫反射的公式:
在这里插入图片描述
以及镜面反射的公式
在这里插入图片描述
然后将着色器中的代码修改成如图所示:
在这里插入图片描述
然后此时可以看到运行结果如图所示:
在这里插入图片描述
c) 参考教材第189页,在着色器中实现改进的Phong反射模型(即Blinn-Phong反射模型)。
根据教材189页可以推导:出以下的模型计算公式
在这里插入图片描述
在这里插入图片描述

使用这个方法计算出来的光照效果要更好一些

d) 加入鼠标控制,用鼠标控制光源位置,实现不同光源位置下的光照效果。
鼠标点击可以得到xy值,而这个xy值的范围是0到500,我们想要将其转换到坐标系范围(-1,1)
在这里插入图片描述
在mouse函数中对函数进行修改,将相机的xy坐标修改成这个范围内的
在这里插入图片描述
Main函数中的鼠标回调函数也需要进行修改:
在这里插入图片描述
随后即可通过鼠标点击改变光照的范围:
在这里插入图片描述
在这里可以补充的一点,如果修改主函数调用mouse的函数,并且修改mouse函数参数的内容

void mouse(/*int button,int state,*/int x, int y)//需要修改传入的参数
{
	//if (button == GLUT_LEFT && state == GLUT_DOWN) {
		lightPos = vec3(float(x - 250) / 250, float(250 - y) / 250, 2.0);
	//}
	// TODO 用鼠标控制光源的位置lightPos,以实时更新光照效果
}
//main函数中
	glutPassiveMotionFunc(mouse);//Main函数中的鼠标回调函数也需要进行修改

就可以实现柔和的光照位置随鼠标移动的效果了。

完整代码

/*
*        Computer Graphics Course - Shenzhen University
*    Week 8 - Phong Reflectance Model (per-vertex shading)
* ============================================================
*
* - 本代码仅仅是参考代码,具体要求请参考作业说明,按照顺序逐步完成。
* - 关于配置OpenGL开发环境、编译运行,请参考第一周实验课程相关文档。
*/

#include "include/Angel.h"
#include "include/TriMesh.h"

#pragma comment(lib, "glew32.lib")

#include <cstdlib>
#include <iostream>

using namespace std;

GLuint programID;
GLuint vertexArrayID;
GLuint vertexBufferID;
GLuint vertexNormalID;
GLuint vertexIndexBuffer;

GLuint vPositionID;
GLuint vNormalID;
GLuint modelViewMatrixID;
GLuint modelViewProjMatrixID;

GLuint lightPosID;

TriMesh* mesh = new TriMesh();
vec3 lightPos(0.0, 0.0, 2.0);

//
// 相机参数设置,因为本周不涉及相机观察变换和投影变换,因此可以使用固定相机视角和投影变换。
// 可使用默认设置,注意默认设置不同于单位矩阵。


namespace Camera
{
    mat4 modelMatrix;
    mat4 viewMatrix;
    mat4 projMatrix;

	mat4 ortho( const GLfloat left, const GLfloat right,
		const GLfloat bottom, const GLfloat top,
		const GLfloat zNear, const GLfloat zFar )
	{
		// TODO 请按照实验课内容补全相机观察矩阵的计算
		mat4 c;
		c[0][0] = 2.0 / (right - left);
		c[1][1] = 2.0 / (top - bottom);
		c[2][2] = -2.0 / (zFar - zNear);
		c[3][3] = 1.0;
		c[0][3] = -(right + left) / (right - left);
		c[1][3] = -(top + bottom) / (top - bottom);
		c[2][3] = -(zFar + zNear) / (zFar - zNear);
		return c;
	}

	mat4 perspective( const GLfloat fovy, const GLfloat aspect,
		const GLfloat zNear, const GLfloat zFar)
	{
		// TODO 请按照实验课内容补全相机观察矩阵的计算
		GLfloat top = tan(fovy * M_PI / 180 / 2) * zNear;
		GLfloat right = top * aspect;

		mat4 c;
		c[0][0] = zNear / right;
		c[1][1] = zNear / top;
		c[2][2] = -(zFar + zNear) / (zFar - zNear);
		c[2][3] = -(2.0*zFar*zNear) / (zFar - zNear);
		c[3][2] = -1.0;
		c[3][3] = 0.0;
		return c;
	}

	mat4 lookAt( const vec4& eye, const vec4& at, const vec4& up )
	{
		// TODO 请按照实验课内容补全相机观察矩阵的计算
		vec4 n = normalize(eye - at);
		vec4 u = normalize(vec4(cross(up, n), 0.0));
		vec4 v = normalize(vec4(cross(n, u), 0.0));

		vec4 t = vec4(0.0, 0.0, 0.0, 1.0);
		mat4 c = mat4(u, v, n, t);
		return c * Translate(-eye);
	}
}

//
// OpenGL 初始化

void init()
{
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

	programID = InitShader("vshader_vert.glsl", "fshader_vert.glsl");

	// 从顶点着色器和片元着色器中获取变量的位置
	vPositionID = glGetAttribLocation(programID, "vPosition");
	vNormalID = glGetAttribLocation(programID, "vNormal");
	modelViewMatrixID = glGetUniformLocation(programID, "modelViewMatrix");
	modelViewProjMatrixID = glGetUniformLocation(programID, "modelViewProjMatrix");
	lightPosID = glGetUniformLocation(programID, "lightPos");

	// TODO 读取外部三维模型
	mesh->read_off("sphere_coarse.off");

	vector<vec3f> vs = mesh->v();
	vector<vec3i> fs = mesh->f();
	vector<vec3f> ns;

	// TODO 计算球模型在每个顶点的法向量,并存储到ns数组中
	for (int i = 0; i < vs.size(); ++i) {
		ns.push_back(vs[i] - vec3(0.0, 0.0, 0.0));
	}

	// 生成VAO
	glGenVertexArrays(1, &vertexArrayID);
	glBindVertexArray(vertexArrayID);

	// 生成VBO,并绑定顶点数据
	glGenBuffers(1, &vertexBufferID);
	glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
	glBufferData(GL_ARRAY_BUFFER, vs.size() * sizeof(vec3f), vs.data(), GL_STATIC_DRAW);

	// 生成VBO,并绑定法向量数据
	glGenBuffers(1, &vertexNormalID);
	glBindBuffer(GL_ARRAY_BUFFER, vertexNormalID);
	glBufferData(GL_ARRAY_BUFFER, ns.size() * sizeof(vec3f), ns.data(), GL_STATIC_DRAW);

	// 生成VBO,并绑定顶点索引
	glGenBuffers(1, &vertexIndexBuffer);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vertexIndexBuffer);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, fs.size() * sizeof(vec3i), fs.data(), GL_STATIC_DRAW);

	// OpenGL相应状态设置
	glEnable(GL_LIGHTING);
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LESS);
}

//
// 渲染

void display()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glUseProgram(programID);

	// TODO 计算相机观察矩阵和投影矩阵,并传入顶点着色器
	vec4 eye(0,0, 3.0, 1.0);
	//由于物体在坐标原点,是单位圆,光源在0,0,2的地方,因此我们可以将观察点设置在0,0,3
	vec4 at(0, 0, 0, 1);   // 原点
	vec4 up(0, 1, 0, 0);      // 默认方向
	Camera::modelMatrix = mat4(1.0);
	Camera::viewMatrix = Camera::lookAt(eye, at, up);
	Camera::projMatrix = Camera::perspective(45, 1,0.1,100);
	//使用与3.2实验类似的方法来创建三个矩阵
	mat4 modelViewMatrix = Camera::viewMatrix * Camera::modelMatrix;
	mat4 modelViewProjMatrix = Camera::projMatrix * modelViewMatrix;


	// TODO 将相机位置传入顶点着色器
	glUniformMatrix4fv(modelViewMatrixID, 1, GL_TRUE, &modelViewMatrix[0][0]);
	glUniformMatrix4fv(modelViewProjMatrixID, 1, GL_TRUE,&modelViewProjMatrix[0][0]);

	// TODO 将光源位置传入顶点着色器
	glUniform3fv(lightPosID, 1, &lightPos[0]);

	glEnableVertexAttribArray(vPositionID);
	glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
	glVertexAttribPointer(
		vPositionID,
		3,
		GL_FLOAT,
		GL_FALSE,
		0,
		(void*)0
	);

	glEnableVertexAttribArray(vNormalID);
	glBindBuffer(GL_ARRAY_BUFFER, vertexNormalID);
	glVertexAttribPointer(
		vNormalID,
		3,
		GL_FLOAT,
		GL_FALSE,
		0,
		(void*)0
	);

	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vertexIndexBuffer);

	glDrawElements(
		GL_TRIANGLES,
		int(mesh->f().size() * 3),
		GL_UNSIGNED_INT,
		(void*)0
	);

	glDisableVertexAttribArray(vPositionID);
	glUseProgram(0);

	glutSwapBuffers();
}

//
// 重新设置窗口

void reshape(GLsizei w, GLsizei h)
{
    glViewport(0, 0, w, h);
}

//
// 鼠标响应函数

void mouse(/*int button,int state,*/int x, int y)//需要修改传入的参数
{
	//if (button == GLUT_LEFT && state == GLUT_DOWN) {
		lightPos = vec3(float(x - 250) / 250, float(250 - y) / 250, 2.0);
	//}
	// TODO 用鼠标控制光源的位置lightPos,以实时更新光照效果
}

//
// 键盘响应函数

void keyboard(unsigned char key, int x, int y)
{
	switch(key) 
	{
	case 033:	// ESC键 和 'q' 键退出游戏
		exit(EXIT_SUCCESS);
		break;
	case 'q':
		exit (EXIT_SUCCESS);
		break;
	}
	glutPostRedisplay();
}

//

void idle(void)
{
	glutPostRedisplay();
}

//

void clean()
{
	glDeleteBuffers(1, &vertexBufferID);
	glDeleteProgram(programID);
	glDeleteVertexArrays(1, &vertexArrayID);

	if (mesh) {
		delete mesh;
		mesh = NULL;
	}
}

//

int main(int argc, char **argv)
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
	glutInitWindowSize(500, 500);
	glutCreateWindow("OpenGL-Tutorial");

	glewInit();
	init();

	glutDisplayFunc(display);
	glutReshapeFunc(reshape);
	glutPassiveMotionFunc(mouse);//Main函数中的鼠标回调函数也需要进行修改
	glutKeyboardFunc(keyboard);
	glutIdleFunc(idle);

	glutMainLoop();

	clean();

	return 0;
}

vshader:

#version 330 core

in vec3 vPosition;
in vec3 vNormal;

uniform vec3 lightPos;

uniform mat4 modelViewProjMatrix;
uniform mat4 modelViewMatrix;

out vec4 color;

// Phong 光照模型的实现 (per-vertex shading)

void main()
{
	gl_Position = modelViewProjMatrix * vec4(vPosition, 1.0);

	// TODO 将顶点坐标变换到相机坐标系
	vec4 vertPos_cameraspace = modelViewMatrix * vec4(vPosition, 1.0);
	vec3 V = vertPos_cameraspace.xyz / vertPos_cameraspace.w;
	vec3 N = (modelViewMatrix * vec4(vNormal, 0.0)).xyz;

	
	// TODO 设置三维物体的材质属性
	vec3 ambiColor = vec3(0.2, 0.2, 0.2);
	vec3 diffColor = vec3(0.5, 0.5, 0.5);
	vec3 specColor = vec3(0.3, 0.3, 0.3);

	
	// TODO 计算N,L,V,R四个向量并归一化
	vec3 N_norm = normalize(N);
	vec3 L_norm = normalize(lightPos - V);
	vec3 V_norm = normalize(-V);
	vec3 R_norm = reflect(-L_norm, N_norm);

	// TODO 计算漫反射系数和镜面反射系数
	float lambertian = clamp(dot(L_norm, N_norm), 0.0, 1.0);
	float specular = clamp(dot(R_norm, V_norm), 0.0, 1.0);

	//使用BLINN_PHONG模型
	//vec3 H_norm = normalize(L_norm + V_norm);
	//float specular = clamp(dot(N_norm, H_norm), 0.0, 1.0);
	
	
	// float shininess = 10.0;
	
	// TODO 计算最终每个顶点的输出颜色
	// color = ...; m,,.,
	float d = length(lightPos - V);
	int a=0,b=0,c=1;
	color = vec4( ambiColor+diffColor * lambertian+specColor * pow(specular, 10.0), 1.0);
	//color = vec4( ambiColor+diffColor * lambertian/(a+b*d+c*d*d)+specColor * pow(specular, 10.0)/(a+b*d+c*d*d), 1.0);

}

fshader:

#version 330 core

in vec4 color;

out vec4 fragmentColor;

void main()
{
	fragmentColor = color;
}

实验3.4 Phong反射模型(2)

一、 实验目的

  1. 了解OpenGL中基本的光照模型
  2. 掌握OpenGL中实现基于片元的光照计算

二、 理论背景

一句话总结:利用片元着色器进行对光照的绘制,效果会更加真实。
程序中有三个地方可以执行光照计算:在OpenGL应用程序代码中、在顶点着色器中或者在片元着色器中。无论在何处执行光照计算,都使用相同的基本光照模型,主要区别是在于绘制的效率和外观。
如果在顶点着色器中执行光照计算,那么在光栅化模块中将对顶点的颜色进行插值计算从而得到每个片元的颜色。而本周主要内容是基于片元的光照计算,区别在于片元着色器的输入数据不是顶点着色器计算之后的每个顶点的颜色,而是每个顶点的法向量,在渲染过程中对法向量进行插值而计算片元的颜色,从而输出到光栅化模块中。需要注意的是,输入的每个顶点必须是经过模-视变换和投影变换之后位于裁剪体内部的顶点,因为只有这些顶点最后在光栅化模块中才能得到渲染并显示在绘制窗口中。
如下图所示,左边是采用逐顶点的光照计算的效果,右边采用逐片元光照计算的效果,从结果上来看,因为逐片元计算会对三维物体表面法向量进行差值,从而使得物体表面的法向量场更加均匀,所以产生的光照效果也更加均匀。
在这里插入图片描述

三、 实验内容

请按照如下说明顺序阅读代码并完成实验。
需要说明的是,在OpenGL绘制流水线中,光照计算需要等到三维物体经过相机变换和投影变换之后才能进行,因为此时物体在三维空间中的位置才完全确定下来。另外,通常情况下,我们在相机坐标系下来计算所有向量。具体实验过程如下。
a) 参考实验2.2,读入球模型并绘制。另外,关于OpenGL开发环境配置,详细请参考实验1.1。
b) 在程序中计算球面上每个顶点的法向量,并传入顶点着色器。假设球心位于 ,球面上顶点坐标为 ,那么该点的法向量为 ,注意计算完成后对法向量进行归一化。在程序的初始化阶段,对为法向量创建缓存,并将数据传入到顶点着色器如下。
// 获取三维物体的顶点坐标
vector vs = mesh->v();
vector ns;

// 计算球面上每个顶点的法向量并归一化,默认球心位于坐标系原点
for (int i = 0; i < vs.size(); ++i) {
	ns.push_back(vs[i] - vec3(0.0, 0.0, 0.0));
}

// 从顶点着色器中获取法向量位置,并传入数值
glGenBuffers(1, &vertexNormalID);
glBindBuffer(GL_ARRAY_BUFFER, vertexNormalID);
glBufferData(GL_ARRAY_BUFFER, ns.size() * sizeof(vec3f), ns.data(), GL_STATIC_DRAW);

c) 在顶点着色器中将顶点坐标、光源位置和法向量变换到相机坐标系下。
vec4 vertPos_cameraspace = modelViewMatrix * vec4(vPosition, 1.0);
vec4 lightPos_cameraspace = modelViewMatrix * vec4(lightPos, 1.0);

V = vertPos_cameraspace.xyz / vertPos_cameraspace.w;
lightPos_new = lightPos_cameraspace.xyz / lightPos_cameraspace.w;
N = (modelViewMatrix * vec4(vNormal, 0.0)).xyz;

在上述代码中,首先将顶点位置由模-视变换得到在相机坐标系下的表示,然后需要注意的一点是对输出位置做透视除法以将顶点变换到三维坐标系下。同样,对法向量做类似的计算。

d) 在片元着色器中计算上述环境光反射,漫反射和镜面反射分量。
首先在着色器中定义环境光反射系数、漫反射系数和镜面反射系数的每个颜色分量(ka,kd,ks)如下,
vec3 ambiColor = vec3(0.1, 0.1, 0.1);
vec3 diffColor = vec3(0.5, 0.5, 0.5);
vec3 specColor = vec3(0.3, 0.3, 0.3);
然后计算Phong反射模型涉及到的四个向量,并归一化,其中使用的normalize函数,reflect函数均为GLSL语言内置函数,分别是归一化向量和依据入射向量和法向量计算反射向量。如下所示:
vec3 N_norm = normalize(N);
vec3 L_norm = normalize(lightPos_new - V);
vec3 V_norm = normalize(-V);
vec3 R_norm = reflect(-L_norm, N_norm);
依据上述公式,计算漫反射分量和镜面反射分量如下,注意代码中使用了GLSL语言内置函数clamp,主要作用是将函数值裁剪到规定的两个值之间,具体使用方法请参考相关书籍。
float lambertian = clamp(dot(L_norm, N_norm), 0.0, 1.0);
float specular = clamp(dot(R_norm, V_norm), 0.0, 1.0);
最后,我们将上述几个部分相加,得到最终输出的顶点颜色值如下。
fragmentColor = vec4(ambiColor +
diffColor * lambertian +
specColor * pow(specular, 5.0), 1.0);

四、 示例和练习

a) 在片元着色器中,分别只用环境光反射分量、漫反射分量和镜面反射分量作为最后的光照结果,观察输出。
b) 上述代码示例中的实现的光照模型并没有考虑到光源和物体表面顶点之间距离的影响,请尝试加入这一项。
c) 参考教材第189页,在着色器中实现改进的Phong反射模型(即Blinn-Phong反射模型)。
d) 加入鼠标控制,用鼠标控制光源位置,实现不同光源位置下的光照效果。

按照上述实验步骤,读入sphere_coarse.off模型,得到的光照效果如下图所示。

(请注意和上一节课程最终光照结果的区别,本节基于片元的光照结果会更加光滑。)
在这里插入图片描述

完整代码

与3.3完全类似,只是着色器代码变了而已,因此此处只放出vshader和fshader的代码

#version 330 core

in vec3 vPosition;
in vec3 vNormal;

uniform vec3 lightPos;
uniform mat4 modelViewProjMatrix;
uniform mat4 modelViewMatrix;

out vec3 N;
out vec3 V;
out vec3 lightPos_new;

// Phong 光照模型的实现 (per-fragment shading)

void main()
{
	gl_Position = modelViewProjMatrix * vec4(vPosition, 1.0);

	// TODO 将顶点变换到相机坐标系下
	vec4 vertPos_cameraspace = modelViewMatrix * vec4(vPosition, 1.0);

	// 对顶点坐标做透视投影
	V = vertPos_cameraspace.xyz / vertPos_cameraspace.w;


	// TODO 将光源位置变换到相机坐标系下
	vec4 lightPos_cameraspace = modelViewMatrix * vec4(lightPos, 1.0);

	// 对光源坐标做透视投影
	lightPos_new = lightPos_cameraspace.xyz / lightPos_cameraspace.w;
	
	// TODO 将法向量变换到相机坐标系下并传入片元着色器
	N = (modelViewMatrix * vec4(vNormal, 0.0)).xyz;
}

fshader:

#version 330 core

in vec3 N;
in vec3 V;
in vec3 lightPos_new;

out vec4 fragmentColor;

void main()
{
	// TODO 设置三维物体的材质属性

	vec3 ambiColor = vec3(0.1, 0.1, 0.1);
	vec3 diffColor = vec3(0.5, 0.5, 0.5);
	vec3 specColor = vec3(0.3, 0.3, 0.3);

	// TODO 计算N,L,V,R四个向量并归一化
	vec3 N_norm = normalize(N);
	vec3 L_norm = normalize(lightPos_new - V);
	vec3 V_norm = normalize(-V);
	vec3 R_norm = reflect(-L_norm, N_norm);

	// TODO 计算漫反射系数和镜面反射系数
	float lambertian = clamp(dot(L_norm, N_norm), 0.0, 1.0);
	//float specular = clamp(dot(R_norm, V_norm), 0.0, 1.0);
	
	//使用BLINN_PHONG模型
	vec3 H_norm = normalize(L_norm + V_norm);
	float specular = clamp(dot(N_norm, H_norm), 0.0, 1.0);

	float shininess = 10.0;
	float d = length(lightPos_new - V);
	int a=0,b=0,c=1;
	// TODO 计算最终每个片元的输出颜色
	fragmentColor = vec4(ambiColor + 
				   diffColor * lambertian +
				   specColor * pow(specular, 5.0), 1.0);
	//fragmentColor=vec4(ambiColor+diffColor * lambertian/(a+b*d+c*d*d)+specColor * pow(specular, 5.0)/(a+b*d+c*d*d), 1.0);

}

实验三 光照与阴影

一、 实验内容
实现场景的光照和阴影。示例场景是一个球,需要达到的目标是实现对几何体的光照并以光源为投影中心生成阴影,如下图所示:
在这里插入图片描述
具体内容包括:

  1. 场景绘制和背景色设置
    创建OpenGL绘制窗口,然后参考实验2.2内容读入三维场景文件(实验课程提供正方体,球等简单几何体的*.off文件)并绘制。为了和后期的阴影颜色区分,将窗口背景色设置为灰色。
  2. 添加光照效果
    参考实验3.3或实验3.4,实现Phong光照效果。
  3. 添加阴影效果
    参考实验3.2,以步骤2中的光源位置作为投影中心,自定义投影平面(为计算方便,推荐使用y=0平面),计算阴影投影矩阵,为三维物体生成阴影。
  4. 鼠标交互控制光源位置并更新阴影
    参考实验2.1,加上鼠标点击,控制光源位置并更新光照效果。另外,由于光源位置同时表示投影中心,所以同时计算三维物体的阴影。

实验三代码

以下绘制阴影采用的是多使用了一个着色器的方法,下面有另外一种方法,就是使用bool变量

#include "include/Angel.h"
#include "include/TriMesh.h"

#include <cstdlib>
#include <iostream>

#pragma comment(lib, "glew32.lib")

using namespace std;

GLuint programID;
GLuint vertexArrayID;
GLuint vertexBufferID;
GLuint vertexNormalID;
GLuint vertexIndexBuffer;

GLuint vPositionID;
GLuint vNormalID;
GLuint modelViewMatrixID;
GLuint modelViewProjMatrixID;

GLuint lightPosID;

GLuint ShadowProgramID;


GLuint shadowmodelMatrixID;
GLuint shadowviewMatrixID;
GLuint shadowprojMatrixID;

GLuint vShadowPositionID;

float rotationAngle = 0;

float cameraXpos = 0;
float cameraYpos = 0;

float cameraZpos = 0;


TriMesh* mesh = new TriMesh();
vec3 lightPos(-0.5, 1.5, 2.0);

//
// 相机参数设置

namespace Camera
{
	mat4 modelMatrix;
    mat4 viewMatrix;
    mat4 projMatrix;

	mat4 ortho( const GLfloat left, const GLfloat right,
		const GLfloat bottom, const GLfloat top,
		const GLfloat zNear, const GLfloat zFar )
	{
		// TODO 请按照实验课内容补全相机观察矩阵的计算
		mat4 c;
		c[0][0] = 2.0 / (right - left);
		c[1][1] = 2.0 / (top - bottom);
		c[2][2] = -2.0 / (zFar - zNear);
		c[3][3] = 1.0;
		c[0][3] = -(right + left) / (right - left);
		c[1][3] = -(top + bottom) / (top - bottom);
		c[2][3] = -(zFar + zNear) / (zFar - zNear);
		return c;
	}

	mat4 perspective( const GLfloat fovy, const GLfloat aspect,
		const GLfloat zNear, const GLfloat zFar)
	{
		// TODO 请按照实验课内容补全相机观察矩阵的计算
		GLfloat top = tan(fovy * M_PI / 180 / 2) * zNear;
		GLfloat right = top * aspect;

		mat4 c;
		c[0][0] = zNear / right;
		c[1][1] = zNear / top;
		c[2][2] = -(zFar + zNear) / (zFar - zNear);
		c[2][3] = -(2.0*zFar*zNear) / (zFar - zNear);
		c[3][2] = -1.0;
		c[3][3] = 0.0;
		return c;
	}

	mat4 lookAt( const vec4& eye, const vec4& at, const vec4& up )
	{
		// TODO 请按照实验课内容补全相机观察矩阵的计算
		vec4 n = normalize(eye - at);
		vec4 u = normalize(vec4(cross(up, n), 0.0));
		vec4 v = normalize(vec4(cross(n, u), 0.0));

		vec4 t = vec4(0.0, 0.0, 0.0, 1.0);
		mat4 c = mat4(u, v, n, t);
		return c * Translate(-eye);
	}
}

//
// OpenGL 初始化
vector<vec3f> vs;
void init()
{
	glClearColor(0.5f, 0.5f, 0.5f, 0.0f);

	programID = InitShader("vshader_frag.glsl", "fshader_frag.glsl");

	// 从顶点着色器和片元着色器中获取变量的位置
	vPositionID = glGetAttribLocation(programID, "vPosition");
	vNormalID = glGetAttribLocation(programID, "vNormal");
	modelViewMatrixID = glGetUniformLocation(programID, "modelViewMatrix");
	modelViewProjMatrixID = glGetUniformLocation(programID, "modelViewProjMatrix");
	lightPosID = glGetUniformLocation(programID, "lightPos");

	// 读取外部三维模型
	mesh->read_off("sphere.off");

	vs = mesh->v();
	vector<vec3i> fs = mesh->f();
	vector<vec3f> ns;

	// TODO 计算球模型在每个顶点的法向量,并存储到ns数组中
	for (int i = 0; i < vs.size(); ++i) {
		ns.push_back(vs[i] - vec3(0.0, 0.0, 0.0));
	}

	//由于球中心在原点,球有一半在平面以下,因此可以将球整体往上平移0.5方便查看阴影
	for (int i = 0; i < vs.size(); i++) {
		vs[i] += vec3(0.0, 0.5, 0.0);
	}

	// 生成VAO
	glGenVertexArrays(1, &vertexArrayID);
	glBindVertexArray(vertexArrayID);

	// 生成VBO,并绑定顶点数据
	glGenBuffers(1, &vertexBufferID);
	glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
	glBufferData(GL_ARRAY_BUFFER, vs.size() * sizeof(vec3f), vs.data(), GL_STATIC_DRAW);

	// 生成VBO,并绑定法向量数据
	glGenBuffers(1, &vertexNormalID);
	glBindBuffer(GL_ARRAY_BUFFER, vertexNormalID);
	glBufferData(GL_ARRAY_BUFFER, ns.size() * sizeof(vec3f), ns.data(), GL_STATIC_DRAW);

	// 生成VBO,并绑定顶点索引
	glGenBuffers(1, &vertexIndexBuffer);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vertexIndexBuffer);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, fs.size() * sizeof(vec3i), fs.data(), GL_STATIC_DRAW);

	//由于绘制阴影和绘制球所用到的着色器不同,因此我们可以换一个着色器来使用
	ShadowProgramID = InitShader("vshader_shadow.glsl", "fshader_shadow.glsl");

	// 从顶点着色器和片元着色器中获取变量的位置
	vShadowPositionID = glGetAttribLocation(ShadowProgramID, "vPosition");
	shadowmodelMatrixID = glGetUniformLocation(ShadowProgramID, "modelMatrix");
	shadowviewMatrixID = glGetUniformLocation(ShadowProgramID, "viewMatrix");
	shadowprojMatrixID = glGetUniformLocation(ShadowProgramID, "projMatrix");


	// OpenGL相应状态设置
	glEnable(GL_LIGHTING);
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LESS);
}

//
// 渲染

void display()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glUseProgram(programID);
	// 将光源位置传入顶点着色器

	// TODO 计算相机观察矩阵和投影矩阵,并传入顶点着色器
	vec4 eye(0.0, 3.0, 3.0, 1.0);
	//由于物体在坐标原点,是单位圆,光源在0,0,2的地方,因此我们可以将观察点设置在0,0,3
	vec4 at(0, 0, 0, 1);   // 原点
	vec4 up(0, 1, 0, 0);      // 默认方向
	Camera::modelMatrix = mat4(1.0)*Translate(cameraXpos,cameraYpos,cameraZpos);
	Camera::viewMatrix = Camera::lookAt(eye, at, up);
	Camera::projMatrix = Camera::perspective(45, 1, 0.1, 100);
	//使用与3.2实验类似的方法来创建三个矩阵
	mat4 modelViewMatrix = Camera::viewMatrix * Camera::modelMatrix;
	mat4 modelViewProjMatrix = Camera::projMatrix * modelViewMatrix;


	// TODO 将相机位置传入顶点着色器
	glUniformMatrix4fv(modelViewMatrixID, 1, GL_TRUE, &modelViewMatrix[0][0]);
	glUniformMatrix4fv(modelViewProjMatrixID, 1, GL_TRUE, &modelViewProjMatrix[0][0]);



	glUniform3fv(lightPosID, 1, &lightPos[0]);

	glEnableVertexAttribArray(vPositionID);
	glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
	glVertexAttribPointer(
		vPositionID,
		3,
		GL_FLOAT,
		GL_FALSE,
		0,
		(void*)0
	);

	glEnableVertexAttribArray(vNormalID);
	glBindBuffer(GL_ARRAY_BUFFER, vertexNormalID);
	glVertexAttribPointer(
		vNormalID,
		3,
		GL_FLOAT,
		GL_FALSE,
		0,
		(void*)0
	);

	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vertexIndexBuffer);

	glDrawElements(
		GL_TRIANGLES,
		int(mesh->f().size() * 3),
		GL_UNSIGNED_INT,
		(void*)0
	);

	glDisableVertexAttribArray(vPositionID);
	glUseProgram(0);



	//
	//此处为我自己添加的部分
	glUseProgram(ShadowProgramID);

	float lx = lightPos[0];
	float ly = lightPos[1];
	float lz = lightPos[2];

	//计算阴影的矩阵
	mat4 shadowProjMatrix(-ly, 0.0, 0.0, 0.0,
		lx, 0.0, lz, 1.0,
		0.0, 0.0, -ly, 0.0,
		0.0, 0.0, 0.0, -ly);

	//分别传入三个矩阵
	glUniformMatrix4fv(shadowmodelMatrixID, 1, GL_TRUE, shadowProjMatrix*Camera::modelMatrix );
	glUniformMatrix4fv(shadowviewMatrixID, 1, GL_TRUE, Camera::viewMatrix);
	glUniformMatrix4fv(shadowprojMatrixID, 1, GL_TRUE, Camera::projMatrix);


	glEnableVertexAttribArray(vShadowPositionID);//此处复制上面的代码,只是把vpositionID换成了shadowPositionID
	glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
	glVertexAttribPointer(
		vShadowPositionID,
		3,
		GL_FLOAT,
		GL_FALSE,
		0,
		(void*)0
	);

	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vertexIndexBuffer);
	glDrawElements(
		GL_TRIANGLES,
		int(mesh->f().size() * 3),
		GL_UNSIGNED_INT,
		(void*)0
	);

	glDisableVertexAttribArray(vShadowPositionID);
	glUseProgram(0);



	glutSwapBuffers();
}

//
// 重新设置窗口

void reshape(GLsizei w, GLsizei h)
{
    glViewport(0, 0, w, h);
}

//
// 鼠标响应函数
//vec3 lightPos(-0.5, 1.5, 2.0);
void mouse(int button, int state, int x, int y)
{
	// TODO 用鼠标控制光源的位置,并实时更新光照效果
	if (state == GLUT_DOWN) {
		int h = glutGet(GLUT_WINDOW_HEIGHT);
		int w = glutGet(GLUT_WINDOW_WIDTH);
		int pos = (h - y) - h / 2;
		float delta = float(pos) / float(h);

		lightPos[1] = 1.5 + delta;//调整光在y方向上的变化
	}

	glutPostRedisplay();
}


//
// 键盘响应函数

void keyboard(unsigned char key, int x, int y)
{
	switch(key) 
	{
	case 033:	// ESC键 和 'q' 键退出游戏
		exit(EXIT_SUCCESS);
		break;
	case 'w':
		cameraYpos += 0.1;
		break;
	case 'W':
		cameraYpos += 0.1;
		break;
	case 's':
		cameraYpos -= 0.1;
		if (cameraYpos <= 0)cameraYpos = 0;
		break;
	case 'S':
		cameraXpos -= 0.1;
		break;
	case 'a':
		cameraXpos -= 0.1;
		break;
	case 'A':
		cameraXpos -= 0.1;
		break;
	case 'd':
		cameraXpos += 0.1;
		break;
	case 'D':
		cameraXpos += 0.1;
		break;
	case 'r':
		cameraXpos = 0;
		cameraYpos = 0;
		break;
	case 'q':
		cameraZpos -= 0.1;
		break;
	case 'Q':
		cameraZpos -= 0.1;
		break;
	case 'e':
		cameraZpos += 0.1;
		break;
	case 'E':
		cameraZpos += 0.1;
		break;


	case 'R':		
		cameraXpos = 0;
		cameraYpos = 0;
		break;
	}
	glutPostRedisplay();
}

//

void idle(void)
{
	glutPostRedisplay();
}

//

void clean()
{
	glDeleteBuffers(1, &vertexBufferID);
	glDeleteProgram(programID);
	glDeleteVertexArrays(1, &vertexArrayID);

	if (mesh) {
		delete mesh;
		mesh = NULL;
	}
}

//

int main(int argc, char **argv)
{
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
	glutInitWindowSize(500, 500);
	glutCreateWindow("OpenGL-Tutorial");

	glewInit();
	init();

	glutDisplayFunc(display);
	glutReshapeFunc(reshape);

	glutMouseFunc(mouse);
	glutKeyboardFunc(keyboard);
	glutIdleFunc(idle);

	glutMainLoop();

	clean();

	return 0;
}

顶点着色器:

#version 330 core

in vec3 vPosition;
in vec3 vNormal;

uniform vec3 lightPos;
uniform mat4 modelViewProjMatrix;
uniform mat4 modelViewMatrix;

out vec3 N;
out vec3 V;
out vec3 lightPos_new;

// Phong 光照模型的实现 (per-fragment shading)

void main()
{
	gl_Position = modelViewProjMatrix * vec4(vPosition, 1.0);

	// TODO 将顶点变换到相机坐标系下
	vec4 vertPos_cameraspace = modelViewMatrix * vec4(vPosition, 1.0);

	// 对顶点坐标做透视投影
	V = vertPos_cameraspace.xyz / vertPos_cameraspace.w;


	// TODO 将光源位置变换到相机坐标系下
	vec4 lightPos_cameraspace = modelViewMatrix * vec4(lightPos, 1.0);

	// 对光源坐标做透视投影
	lightPos_new = lightPos_cameraspace.xyz / lightPos_cameraspace.w;
	
	// TODO 将法向量变换到相机坐标系下并传入片元着色器
	N = (modelViewMatrix * vec4(vNormal, 0.0)).xyz;
}

片元着色器:

#version 330 core

in vec3 N;
in vec3 V;
in vec3 lightPos_new;

out vec4 fragmentColor;

void main()
{
	// TODO 设置三维物体的材质属性

	vec3 ambiColor = vec3(0.1, 0.1, 0.1);
	vec3 diffColor = vec3(0.5, 0.5, 0.5);
	vec3 specColor = vec3(0.3, 0.3, 0.3);

	// TODO 计算N,L,V,R四个向量并归一化
	vec3 N_norm = normalize(N);
	vec3 L_norm = normalize(lightPos_new - V);
	vec3 V_norm = normalize(-V);
	vec3 R_norm = reflect(-L_norm, N_norm);

	// TODO 计算漫反射系数和镜面反射系数
	float lambertian = clamp(dot(L_norm, N_norm), 0.0, 1.0);
	//float specular = clamp(dot(R_norm, V_norm), 0.0, 1.0);
	
	//使用BLINN_PHONG模型
	vec3 H_norm = normalize(L_norm + V_norm);
	float specular = clamp(dot(N_norm, H_norm), 0.0, 1.0);

	float shininess = 10.0;
	float d = length(lightPos_new - V);
	int a=0,b=0,c=1;
	// TODO 计算最终每个片元的输出颜色
	fragmentColor = vec4(ambiColor + 
				   diffColor * lambertian +
				   specColor * pow(specular, 5.0), 1.0);
	//fragmentColor=vec4(ambiColor+diffColor * lambertian/(a+b*d+c*d*d)+specColor * pow(specular, 5.0)/(a+b*d+c*d*d), 1.0);

}

顶点阴影着色器:

#version 330 core

in vec3 vPosition;

uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projMatrix;
out vec4 fColor;
void main()
{
	vec4 v1 = modelMatrix*vec4(vPosition, 1.0);  
	// 由于modelMatrix有可能为阴影矩阵,为了得到正确位置,我们需要做一次透视除法
	vec4 v2 = vec4(v1.xyz / v1.w, 1.0);
	// 考虑相机和投影
	vec4 v3 = projMatrix*viewMatrix*v2;
	// 再做一次透视除法 (为了明确概念,我们显式做一次透视除法)
	vec4 v4 = vec4(v3.xyz / v3.w, 1.0);
	gl_Position = v4;
	fColor =vec4(0.0,0.0,0.0,1.0);//传出黑色作为阴影
}

片元阴影着色器:

#version 330 core
in vec4 fColor;
out vec4 color;

void main() 
{ 
    color = fColor;
} 

效果:
在这里插入图片描述
另外一种方法是使用传入bool变量的方式:

	GLuint drawShadowID;
	drawShadowID = glGetUniformLocation(programID, "drawShadow");
	
	//在绘制物体和绘制阴影的时候都需要传给drawshadow这个变量值
	glUniform1i(drawShadowID, 0);

然后在片元着色器中,如果是阴影则需要:

	if (drawShadow)
		color = vec4(shadowColor, 1.0); //shadowColor

vshader:

#version 330 core 

in vec3 vPosition;
in vec3 vNormal;

out vec3 N;
out vec3 V;
out vec3 L;

uniform vec3 lightPos;

//uniform mat4 transformMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 projMatrix;

void main() 
{
	vec4 v1 = projMatrix * modelViewMatrix * vec4(vPosition, 1.0);
	vec4 v2 = vec4(v1.xyz / v1.w, 1.0);
	
	//gl_Position = transformMatrix * v2;
	gl_Position = v2;
	
	//
	
	vec4 vertPos_cameraspace = modelViewMatrix * vec4(vPosition, 1.0);
	V = vertPos_cameraspace.xyz / vertPos_cameraspace.w;

	vec4 lightPos_cameraspace = modelViewMatrix * vec4(lightPos, 1.0);
	L = lightPos_cameraspace.xyz / lightPos_cameraspace.w;

	N = (modelViewMatrix * vec4(vNormal, 0.0)).xyz;
}

片元着色器:

#version 330 core 

in vec3 vPosition;
in vec3 vNormal;

out vec3 N;
out vec3 V;
out vec3 L;

uniform vec3 lightPos;

//uniform mat4 transformMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 projMatrix;

void main() 
{
	vec4 v1 = projMatrix * modelViewMatrix * vec4(vPosition, 1.0);
	vec4 v2 = vec4(v1.xyz / v1.w, 1.0);
	
	//gl_Position = transformMatrix * v2;
	gl_Position = v2;
	
	//
	
	vec4 vertPos_cameraspace = modelViewMatrix * vec4(vPosition, 1.0);
	V = vertPos_cameraspace.xyz / vertPos_cameraspace.w;

	vec4 lightPos_cameraspace = modelViewMatrix * vec4(lightPos, 1.0);
	L = lightPos_cameraspace.xyz / lightPos_cameraspace.w;

	N = (modelViewMatrix * vec4(vNormal, 0.0)).xyz;
}
  • 22
    点赞
  • 67
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
实验为综合实验, 任务是利用光线跟踪算法进行Whitted全局光照计算,并对读入场景进行真实感绘制。(特别提醒: 网上类似的projects可以参考,但不能照抄. 如http://tobias.isenberg.cc/graphics/LabSessions/RaytracingProject, http://physbam.stanford.edu/links/ray_tracing/project_ray_tracing.html https://www.cs.utexas.edu/~fussell/courses/cs354/assignments/raytracing/handout.shtml ) (1) 参加对象: 本实验针对所有选课同,3-5人组成一个小组,共同实现;非15级同在组队方面有困难的话可与老师沟通. (2) 实验结果提交: 每人都要求提交一份. 内容包括 a. 源程序; 可执行代码; 三维场景数据; 同组的同这部分可以相同. b. 实验报告; c. ppt一份, 应该有绘制结果的视频展示. (3) 功能方面的基本要求: a. 实现光线跟踪算法, 能利用Whitted光照模型对场景进行渲染; (optional) 通过亚像素采样实现反混淆. b. 材质: 支持环境光、漫反射、高光反射、透射等光现象; (optional)支持纹理绘制可加分. c. 光源: 在场景中至少有一个点光源, 支持阴影; (optional)增加点光源数可加分). d. 场景: 支持圆和三角网格模型; (optional)增加椭圆、参数曲面可加分. e. 输入输出:读入网格模型文件,保存渲染图像成位图格式(bmp),渲染图像的大小可通过参数调节;(optinal)可读入mtl材质库文件. f. 加速:利用空间划分(八叉树或BSP)进行加速. //压缩包内包含所有需要提交的文件:源程序、可执行代码、实验报告、PPT、三维场景数据、演示视频。 //本材料是2016级华工计院图形课程第三次实验的最终提交压缩包,老实说代码本身并不是我写的,但是除代码之外所有内容是我们小组共同完成的结果。本文件的目的在于给各位找不到头绪的妹们一些借鉴的资料。请勿照搬照抄!希望对你们有所帮助!感谢支持!
3D游戏与计算机图形中的数方法是指在游戏和图形领域中使用的数原理、算法和技术。这些数方法的应用可以帮助我们实现逼真的图形效果和流畅的游戏体验。 在3D游戏中,数方法被广泛应用于处理和渲染3D模型、光照阴影、纹理映射、相机投影等方面。通过应用线性代数、矩阵运算和几何等数方法,我们可以计算出3D模型的位置、旋转和缩放,以及模型之间的碰撞检测和物理模拟。 另外,在计算机图形中,数方法也广泛应用于图形算法和渲染技术的开发中。比如,光线追踪算法需要使用向量和数值计算来模拟光线的传播和反射;反走样算法利用采样理论和随机数生成来减少图形的锯齿感;着色器程序利用向量和矩阵运算来计算光照效果和材质属性等。 此外,数方法还被应用于游戏的物理引擎开发中。物理引擎使用物理的方程和数值计算来模拟游戏中的物体运动、碰撞和力的作用。通过应用牛顿定律、刚体动力和碰撞检测的数方法,我们可以实现真实世界的物理效果,从而让游戏更加逼真和惟妙惟肖。 总之,3D游戏与计算机图形中的数方法是实现逼真图形和流畅游戏的基础。通过应用线性代数、几何、向量和矩阵运算等数原理,我们可以计算、模拟和渲染出令人惊叹的图形效果,同时实现真实世界的物理效果。这些数方法的运用使得游戏和计算机图形领域取得了巨大的发展和进步。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值