源代码:
OpenGL大作业OpenCraft-其他文档类资源-CSDN下载
目录
1. OBJ文件读取
2. 物体渲染与纹理着色。
3. 相机变换的实现
4. 光照与阴影的实现
5. 阴影设计
6. 层级建模
7.添加动画
8. 交互
1.视角移动
2. 鼠标左右键添加删除物体
3. 选择移动、添加的物体
4. 光源位置改变
5. 层级建模交互
9. 其余功能
1. 视角跟随机器人模式
2. 光标制作
我们先看部分OBJ文件内容。
# material
mtllib Ground.mtl
usemtl palette
# normals
vn -1 0 0
vn 1 0 0
vn 0 0 1
vn 0 0 -1
vn 0 -1 0
vn 0 1 0
# texcoords
vt 0.00195313 0.5
vt 0.00585938 0.5
vt 0.00976563 0.5
vt 0.0136719 0.5
vt 0.0175781 0.5
vt 0.0214844 0.5
vt 0.0253906 0.5
# verts
v -50 0 50
v -50 0 49
v -50 0 48
v -50 0 47
v -50 0 46
v -50 0 45
v -50 0 44
v -50 0 43
v -50 0 42
v -50 0 41
v -50 0 40
# faces
f 101/20/1 2/20/1 1/20/1
f 102/41/1 3/41/1 2/41/1
f 102/20/1 2/20/1 101/20/1
f 103/1/1 4/1/1 3/1/1
f 103/41/1 3/41/1 102/41/1
f 104/2/1 5/2/1 4/2/1
f 104/1/1 4/1/1 103/1/1
f 105/48/1 6/48/1 5/48/1
f 105/2/1 5/2/1 104/2/1
f 106/12/1 7/12/1 6/12/1
f 106/48/1 6/48/1 105/48/1
我们看到在每一行的前面都有字母,v代表的是每个点的位置,vn代表的是每个点的法向量,vt代表的是纹理图片的坐标,f是每个面片的信息,由三组(或者四组)以斜杠分隔的整数表示该面片第 i 个顶点的 位置索引/纹理坐标索引/法向量索引
读取时我么需要将里面的数据一次放在vertex_positions, vertex_normals, vertex_textures, faces, texture_index, noemal_index 当中。
while (std::getline(fin, line))
{
std::istringstream sin(line);
std::string type;
GLfloat _x, _y, _z;
int a0, b0, c0;
int a1, b1, c1;
int a2, b2, c2;
char slash;
// 读取obj文件,记录里面的这些数据
sin >> type;
glm::vec3 tmp_node;
if (type == "v")
{
sin >> tmp_node.x >> tmp_node.y >> tmp_node.z;
vertex_positions.push_back(tmp_node);
if (MinPosition > tmp_node.z)
MinPosition = tmp_node.z;
}
if (type == "vn")
{
sin >> tmp_node.x >> tmp_node.y >> tmp_node.z;
vertex_normals.push_back(tmp_node);
vertex_colors.push_back(tmp_node);
}
if (type == "vt")
{
float x, y, z;
sin >> x >> y >> z;
vertex_textures.push_back(glm::vec2(x, y));
}
if (type == "f")
{
sin >> a0 >> slash >> b0 >> slash >> c0;
sin >> a1 >> slash >> b1 >> slash >> c1;
sin >> a2 >> slash >> b2 >> slash >> c2;
faces.push_back(vec3i(a0 - 1, a1 - 1, a2 - 1));
texture_index.push_back(vec3i(b0 - 1, b1 - 1, b2 - 1));
color_index.push_back(vec3i(c0 - 1, c1 - 1, c2 - 1));
normal_index.push_back(vec3i(c0 - 1, c1 - 1, c2 - 1));
}
// 其中vertex_color和color_index可以用法向量的数值赋值
}
此时已经将所有的点的信息传递给了数组当中。
接下来将根据面片顶点坐标,依次加入GPU points等容器中。
在此之前,先将物体的大小进行了归一化处理,也就是让所有的物体尺寸处于同一大小。这个可以通过setNormalize函数进行控制,我们看看若没有进行归一化是怎样的效果。
可以看到wawa的模型十分大,直接包围了table模型。
所以说归一化的操作是十分有必要的,让两个物体大小相差不大。
归一化代码如下:
if (do_normalize_size)
{
// 记录物体包围盒大小,可以用于大小的归一化
// 先获得包围盒的对角顶点
float max_x = -FLT_MAX;
float max_y = -FLT_MAX;
float max_z = -FLT_MAX;
float min_x = FLT_MAX;
float min_y = FLT_MAX;
float min_z = FLT_MAX;
for (int i = 0; i < vertex_positions.size(); i++)
{
auto &position = vertex_positions[i];
if (position.x > max_x)
max_x = position.x;
if (position.y > max_y)
max_y = position.y;
if (position.z > max_z)
max_z = position.z;
if (position.x < min_x)
min_x = position.x;
if (position.y < min_y)
min_y = position.y;
if (position.z < min_z)
min_z = position.z;
}
up_corner = glm::vec3(max_x, max_y, max_z);
down_corner = glm::vec3(min_x, min_y, min_z);
center = glm::vec3((min_x + max_x) / 2.0, (min_y + max_y) / 2.0, (min_z + max_z) / 2.0);
diagonal_length = length(up_corner - down_corner);
minz = FLT_MAX; //找到最低的点
minx = FLT_MAX;
miny = FLT_MAX;
for (int i = 0; i < vertex_positions.size(); i++)
{
vertex_positions[i] = (vertex_positions[i] - center) / diagonal_length;
if (minz > vertex_positions[i].z)
minz = vertex_positions[i].z;
if (miny > vertex_positions[i].y)
miny = vertex_positions[i].y;
if (minx > vertex_positions[i].x)
minx = vertex_positions[i].x;
}
}
然后将点信息放进GPU存储器当中。
for (int i = 0; i < faces.size(); i++)
{
// 坐标
points.push_back(vertex_positions[faces[i].x]);
points.push_back(vertex_positions[faces[i].y]);
points.push_back(vertex_positions[faces[i].z]);
// 颜色
colors.push_back(vertex_colors[color_index[i].x]);
colors.push_back(vertex_colors[color_index[i].y]);
colors.push_back(vertex_colors[color_index[i].z]);
// 法向量
normals.push_back(vertex_normals[normal_index[i].x]);
normals.push_back(vertex_normals[normal_index[i].y]);
normals.push_back(vertex_normals[normal_index[i].z]);
// 纹理
textures.push_back(vertex_textures[texture_index[i].x]);
textures.push_back(vertex_textures[texture_index[i].y]);
textures.push_back(vertex_textures[texture_index[i].z]);
}
这样子所有的模型存储在GPU当中,准备进行建模。
在main函数中,我编写了一个addMeshes函数,调用这个函数就可以将物体加入到painter当中,使用着色器进行进一步的渲染。
void addMeshes(glm::vec3 Translation, glm::vec3 Rotation, bool setNormalize, std::string Name,
std::string OBJLocation, std::string TextureLocation, glm::vec3 Scall = glm::vec3(1.0, 1.0, 1.0))
{
std::string vshader, fshader;
// 读取着色器并使用
// for Windows
vshader = "shaders/vshader_win.glsl";
fshader = "shaders/fshader_win.glsl";
TriMesh *TMesh = new TriMesh();
TMesh->setNormalize(true);
TMesh->readObj(OBJLocation);
// 设置物体的旋转位移
TMesh->setRotation(Rotation);
TMesh->setScale(Scall);
TMesh->setTranslation(Translation + glm::vec3(0.0, -(TMesh->miny * Scall.y), 0.0)); //使物体始终处于地面上方
TMesh->setAmbient(glm::vec4(0.2, 0.2, 0.2, 1.0)); // 环境光
TMesh->setDiffuse(glm::vec4(0.7, 0.7, 0.7, 1.0)); // 漫反射
TMesh->setSpecular(glm::vec4(0.2, 0.2, 0.2, 1.0)); // 镜面反射
TMesh->setShininess(1.0); //高光系数
// 加到painter中
painter->addMesh(TMesh, Name, TextureLocation, vshader, fshader); // 指定纹理与着色器
}
该函数当中,只需要输入物体初始状态的位置、角度、比例、物体名称、是否需要将物体归一化和物体文件地址和纹理地址。在函数中,调用TriMesh创建一个物体,设置其位置大小比例,再设置其光反射系数。最后将设置好的物体加入到painter当中,调用painter的addMesh函数,准备开始着色。
void MeshPainter::addMesh(TriMesh *mesh, const std::string &name, const std::string &texture_image,
const std::string &vshader, const std::string &fshader)
{
mesh_names.push_back(name);
meshes.push_back(mesh);
openGLObject object;
bindObjectAndData(mesh, object, texture_image, vshader, fshader);
opengl_objects.push_back(object);
};
在addMesh函数当中,首先将物体存入到meshes数组当中,所有产生的单一物体都会存放在meshes函数当中,以便之后的渲染,接着调用bindObjectAndData函数,将物体调用到着色器当中,准备渲染。
接下来,若要增加物体,直接在main文件当中调用addMeshes函数即可。
最后在display函数中,调用painter的drawMeshes函数便可实现物体在窗口当中显示。
void MeshPainter::drawMeshes(Light *light, Camera *camera)
{
drawMesh(meshes[0], opengl_objects[0], light, camera, meshes[0]->getModelMatrix(), 0);
//地面不需要阴影
for (int i = 1; i < meshes.size(); i++)
{
drawMesh(meshes[i], opengl_objects[i], light, camera, meshes[i]->getModelMatrix());
}
};
void MeshPainter::drawMesh(TriMesh *mesh, openGLObject &object,
Light *light, Camera *camera, glm::mat4 modelMatrix)
{
// 相机矩阵计算
camera->updateCamera();
camera->viewMatrix = camera->getViewMatrix();
camera->projMatrix = camera->getProjectionMatrix(true);
#ifdef __APPLE__ // for MacOS
glBindVertexArrayAPPLE(object.vao);
#else
glBindVertexArray(object.vao);
#endif
glUseProgram(object.program);
// 物体的变换矩阵
// 传递矩阵
glUniformMatrix4fv(object.modelLocation, 1, GL_FALSE, &modelMatrix[0][0]);
glUniformMatrix4fv(object.viewLocation, 1, GL_TRUE, &camera->viewMatrix[0][0]);
glUniformMatrix4fv(object.projectionLocation, 1, GL_TRUE, &camera->projMatrix[0][0]);
// 将着色器 isShadow 设置为0,表示正常绘制的颜色,如果是1着表示阴影
glUniform1i(object.shadowLocation, 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, object.texture); // 该语句必须,否则将只使用同一个纹理进行绘制
// 传递纹理数据 将生成的纹理传给shader
glUniform1i(glGetUniformLocation(object.program, "texture"), 0);
// 将材质和光源数据传递给着色器
bindLightAndMaterial(mesh, object, light, camera);
// 绘制
glDrawArrays(GL_TRIANGLES, 0, mesh->getPoints().size());
#ifdef __APPLE__ // for MacOS
glBindVertexArrayAPPLE(0);
#else
glBindVertexArray(0);
#endif
glUseProgram(0);
}
经过上述操作,便可实现物体在窗口当中显示。
实现纹理的着色是使用glBindTexture函数,只需要将相应的点与纹理图片,即可使物体附着上纹理。
相机变换的实现封装在了Camera类当中,此次是为了实现FPS第一人称射击游戏那样子的视角交互。
为了实现该功能,我们可以使用欧拉角从而实现。对于欧拉角来说,重要的三个参数是yaw轴,pitch轴和row轴。yaw轴代表的参数是与y轴垂直方向的角度,pitch轴代表的参数是与x轴垂直方向的角度,raw轴是与z轴垂直的角度。在FPS相机当中我们只关心pitch轴和yaw轴。
我们可以根据公式知道使用欧拉角计算相机的朝向,从而将参数传输到cameraDirection当中。
// 计算欧拉角以确定相机的朝向 cameraDirection表示摄像机的朝向向量
float cameraDirectionX = -cos(glm::radians(pitch)) * sin(glm::radians(yaw));
float cameraDirectionY = sin(glm::radians(pitch));
float cameraDirectionZ = cos(glm::radians(pitch)) * cos(glm::radians(yaw));
这样子就可以通过其知道相机的朝向。
关于相机的移动,我们可以通过更改eye参数进行。对于向前,向后,我们可以根据欧拉角的yaw轴来进行计算。
// 键盘事件处理
// 通过按键改变相机和投影的参数
//通过计算,可以使的一直以相机的位置进行移动
if (key == GLFW_KEY_A && action == GLFW_PRESS && mode == 0x0000 || key == GLFW_KEY_A && action == GLFW_REPEAT && mode == 0x0000)
{
eyez += cos((yaw - 90) / 180 * PI) * 0.1;
eyex -= sin((yaw - 90) / 180 * PI) * 0.1;
}
else if (key == GLFW_KEY_D && action == GLFW_PRESS && mode == 0x0000 || key == GLFW_KEY_D && action == GLFW_REPEAT && mode == 0x0000)
{
eyez += cos((yaw + 90) / 180 * PI) * 0.1;
eyex -= sin((yaw + 90) / 180 * PI) * 0.1;
}
else if (key == GLFW_KEY_W && action == GLFW_PRESS && mode == 0x0000 || key == GLFW_KEY_W && action == GLFW_REPEAT && mode == 0x0000)
{
eyez += cos(yaw / 180 * PI) * 0.1;
eyex -= sin(yaw / 180 * PI) * 0.1;
}
else if (key == GLFW_KEY_S && action == GLFW_PRESS && mode == 0x0000 || key == GLFW_KEY_S && action == GLFW_REPEAT && mode == 0x0000)
{
eyez -= cos(yaw / 180 * PI) * 0.1;
eyex += sin(yaw / 180 * PI) * 0.1;
}
按照上述编写,即可实现相机根据yaw轴,始终前进的时候是按照相机的欧拉角方向进行前进。
使用相机矩阵时,先进行相机的参数更新,再是获取相机的视角矩阵,最后获得渲染的矩阵。
// 相机矩阵计算
camera->updateCamera();
camera->viewMatrix = camera->getViewMatrix();
camera->projMatrix = camera->getProjectionMatrix(true);
void Camera::updateCamera()
{
// 设置相机位置和方向
up = glm::vec4(0.0, 1.0, 0.0, 0.0);
// 计算欧拉角以确定相机的朝向 cameraDirection表示摄像机的朝向向量
float cameraDirectionX = -cos(glm::radians(pitch)) * sin(glm::radians(yaw));
float cameraDirectionY = sin(glm::radians(pitch));
float cameraDirectionZ = cos(glm::radians(pitch)) * cos(glm::radians(yaw));
// 确定每时每刻的对应的相机的朝向
cameraDirection = glm::vec4(cameraDirectionX, cameraDirectionY, cameraDirectionZ, 1.0);
eye = glm::vec4(eyex, eyey, eyez, 1.0);
}
对于更改相机视角的欧拉角,是通过下面mouse函数进行,从而对yaw轴和pitch轴进行设置。
//通过对应输入的x和y变量,变动yaw轴和pitch轴的变量
void Camera::mouse(double x, double y)
{
yaw += x;
pitch += y;
const float AEdis = 2.4;
float dis;
if (eyey > AEdis)
dis = 89.5f;
else
dis = 90 - acos(eyey / AEdis) / PI * 180; //此计算是为了让视角一直保持在地面上方
if (pitch > dis)
pitch = dis;
if (pitch < -89.0f)
pitch = -89.0f;
}
在上述操作当中,我们实际上已经将光照设计好了,首先我们要确定光照的位置。通过Light类来进行确定,直接调用light相关函数进行设置。
// 设置光源位置
light->setTranslation(glm::vec3(0.0, 40.0, 20.0));
light->setAmbient(glm::vec4(1.0, 1.0, 1.0, 1.0)); // 环境光
light->setDiffuse(glm::vec4(1.0, 1.0, 1.0, 1.0)); // 漫反射
light->setSpecular(glm::vec4(1.0, 1.0, 1.0, 1.0)); // 镜面反射
light->setAttenuation(1.0, 0.045, 0.0075); // 衰减系数
然后在drawMesh函数当中调用bindLightAndMaterial函数即可实现。
void MeshPainter::bindLightAndMaterial(TriMesh *mesh, openGLObject &object, Light *light, Camera *camera)
{
// 传递材质、光源等数据给着色器
// 传递相机的位置
glUniform3fv(glGetUniformLocation(object.program, "eye_position"), 1, &camera->eye[0]);
// 传递物体的材质
glm::vec4 meshAmbient = mesh->getAmbient();
glm::vec4 meshDiffuse = mesh->getDiffuse();
glm::vec4 meshSpecular = mesh->getSpecular();
float meshShininess = mesh->getShininess();
glUniform4fv(glGetUniformLocation(object.program, "material.ambient"), 1, &meshAmbient[0]);
glUniform4fv(glGetUniformLocation(object.program, "material.diffuse"), 1, &meshDiffuse[0]);
glUniform4fv(glGetUniformLocation(object.program, "material.specular"), 1, &meshSpecular[0]);
glUniform1f(glGetUniformLocation(object.program, "material.shininess"), meshShininess);
// 传递光源信息
glm::vec4 lightAmbient = light->getAmbient();
glm::vec4 lightDiffuse = light->getDiffuse();
glm::vec4 lightSpecular = light->getSpecular();
glm::vec3 lightPosition = light->getTranslation();
glUniform4fv(glGetUniformLocation(object.program, "light.ambient"), 1, &lightAmbient[0]);
glUniform4fv(glGetUniformLocation(object.program, "light.diffuse"), 1, &lightDiffuse[0]);
glUniform4fv(glGetUniformLocation(object.program, "light.specular"), 1, &lightSpecular[0]);
glUniform3fv(glGetUniformLocation(object.program, "light.position"), 1, &lightPosition[0]);
glUniform1f(glGetUniformLocation(object.program, "light.constant"), light->getConstant());
glUniform1f(glGetUniformLocation(object.program, "light.linear"), light->getLinear());
glUniform1f(glGetUniformLocation(object.program, "light.quadratic"), light->getQuadratic());
}
此时若要实现Phone光照模型,则需要在fshader和vshader当中进行更改。
在fshader当中,我们将以下代码加入到不是阴影时的当中。
void main()
{
if (isShadow == 1) {
fColor = vec4(0.0, 0.0, 0.0, 0.5);
} else {
//计算四个归一化的向量 N,V,L,R(或半角向量H)
vec3 N= normalize(normal);
vec3 V= normalize(eye_position - position);
vec3 L= normalize(light.position - position);
vec3 R= normalize(reflect(-L, N));
//计算环境光分量I_a
vec4 I_a = light.ambient * material.ambient;
// 计算漫反射系数alpha和漫反射分量I_d
float diffuse_dot = 0.0;
diffuse_dot = max(dot(L, N), 0);
vec4 I_d = diffuse_dot * light.diffuse * material.diffuse;
// 计算高光系数beta和镜面反射分量I_s
float specular_dot_pow = 0.0;
specular_dot_pow = pow(max(dot(R, V),0), material.shininess);
vec4 I_s = specular_dot_pow * light.specular * material.specular;
// 合并三个分量的颜色,修正透明度
fColor = texture2D( texture, texCoord );
// 叠加phong模型光照颜色
// 修正得到最后的颜色
// 其中id和is加上衰减系数
float d = distance(light.position,position);
// 使用衰减系数进行模拟
fColor += I_a + 1/(light.constant+light.linear*d+light.quadratic*d*d)*(I_d+I_s);
}
}
首先将四个归一化向量修改好。
// 计算四个归一化的向量 N,V,L,R(或半角向量H)
vec3 N= normalize(normal);
vec3 V= normalize(eye_position - position);
vec3 L= normalize(light.position - position);
vec3 R= normalize(reflect(-L, N));
根据漫反射公式
//计算漫反射系数alpha和漫反射分量I_d
float diffuse_dot = 0.0;
diffuse_dot = max(dot(L, N), 0);
vec4 I_d = diffuse_dot * light.diffuse * material.diffuse;
再根据镜面反射公式
// 计算高光系数beta和镜面反射分量I_s
float specular_dot_pow = 0.0;
specular_dot_pow = pow(max(dot(R, V),0), material.shininess);
vec4 I_s = specular_dot_pow * light.specular * material.specular;
这样子phong光照模型就计算好了。
在drawMesh函数当中,当物体渲染完毕后就对阴影进行渲染,将以下代码加入到drawMesh函数后即可实现。
//接下来是对阴影的设置。
glm::vec4 light_position = light->getLightPosition();
float ly = light_position[1];
glBindVertexArray(object.vao);
glUseProgram(object.program);
modelMatrix = light->getShadowProjectionMatrix() * modelMatrix;
glUniformMatrix4fv(object.modelLocation, 1, GL_FALSE, &modelMatrix[0][0]);
glUniformMatrix4fv(object.viewLocation, 1, GL_TRUE, &camera->viewMatrix[0][0]);
glUniformMatrix4fv(object.projectionLocation, 1, GL_TRUE, &camera->projMatrix[0][0]);
glUniform1i(object.shadowLocation, 1);
glDrawArrays(GL_TRIANGLES, 0, mesh->getPoints().size());
#ifdef __APPLE__ // for MacOS
glBindVertexArrayAPPLE(0);
#else
glBindVertexArray(0);
#endif
glUseProgram(0);
通过调用light的getShadowProjectionMatrix即可。
进行层级建模时我们首先要构思模型的各个结构,以下是层级建模的大致框架。
以上是层级建模的大致框架。首先我们需要将一些参数进行绑定。我写了一个Robot的结构体,这样可以方便的查找数据。
// Robot类,用于储存层级建模模型的关键信息
struct Robot
{
// 关节大小
float BODY_HEIGHT = 2.5;
float BODY_WIDTH = 1.5;
float BIG_ARM_HEIGHT = 1.5;
float SMALL_ARM_HEIGHT = 1.0;
float BIG_ARM_WIDTH = 0.8;
float HAND_HEIGHT = 0.6;
float SMALL_ARM_WIDTH = 0.5;
float SWORD_HEIGHT = 2.2;
float HAND_WIDTH = 0.6;
float SWORD_WIDTH = 0.5;
float HEAD_HEIGHT = 1.2;
float HEAD_WIDTH = 1.2;
// 关节角和菜单选项值
enum
{
Body, // 躯干
Head, // 头部
RightBigArm, // 右大臂
RightSmallArm, // 右小臂
LeftBigArm, // 左大臂
LeftSmallArm, // 左小臂
RightHand, // 右手
RightSword, // 右剑
LeftHand, // 左手
LeftSword, // 左剑
};
// 关节角大小
GLfloat theta[10] = {
0.0, // Body
0.0, // Head
0.0, // RightBigArm
-90.0, // RightSmallArm
0.0, // LeftBigArm
-90.0, // LeftSmallArm
0.0, // RightHand
0.0, // RightSword
0.0, // LeftHand
0.0 // LeftSword
};
};
接下来,我是使用栈的方式进行建模,所以构造了一个矩阵栈。
//矩阵栈,用来存放矩阵的栈
class MatrixStack
{
int _index;
int _size;
glm::mat4 *_matrices;
public:
MatrixStack(int numMatrices = 100) : _index(0), _size(numMatrices)
{
_matrices = new glm::mat4[numMatrices];
}
~MatrixStack()
{
delete[] _matrices;
}
void push(const glm::mat4 &m)
{
assert(_index + 1 < _size);
_matrices[_index++] = m;
}
glm::mat4 &pop()
{
assert(_index - 1 >= 0);
_index--;
return _matrices[_index];
}
};
接下来,使用TriMesh数组,对层级建模的每一个物体进行绑定,并且在此定义object准备绑定。
Robot robot;
std::vector Man;
openGLObject BodyObject;
openGLObject HeadObject;
openGLObject RightBigArmObject;
openGLObject RightSmallArmObject;
openGLObject LeftBigArmObject;
openGLObject LeftSmallArmObject;
openGLObject RightHandObject;
openGLObject RightSwordObject;
openGLObject LeftHandObject;
openGLObject LeftSwordObject;
在init函数当中,对所有的层级建模物体进行初始化。
//添加层级建模物体
for (int i = 0; i < MANNUM; i++)
{
TriMesh *Mesh = new TriMesh();
Mesh->setTranslation(glm::vec3(0.0, 0.0, 0.0));
Mesh->setRotation(glm::vec3(0.0, 0.0, 0.0));
Mesh->setScale(glm::vec3(1.0, 1.0, 1.0));
Mesh->setNormalize(true);
Mesh->setAmbient(glm::vec4(0.2, 0.2, 0.2, 1.0)); // 环境光
Mesh->setDiffuse(glm::vec4(0.7, 0.7, 0.7, 1.0)); // 漫反射
Mesh->setSpecular(glm::vec4(0.2, 0.2, 0.2, 1.0)); // 镜面反射
Mesh->setShininess(1.0); //高光系数
Man.push_back(Mesh);
}
Man[robot.Body]->readObj("./assets/Man/Body.obj");
Man[robot.LeftBigArm]->readObj("./assets/Man/BigArm.obj");
Man[robot.RightBigArm]->readObj("./assets/Man/BigArm.obj");
Man[robot.LeftSmallArm]->readObj("./assets/Man/SmallArm.obj");
Man[robot.RightSmallArm]->readObj("./assets/Man/SmallArm.obj");
Man[robot.RightHand]->readObj("./assets/Man/hands.obj");
Man[robot.LeftHand]->readObj("./assets/Man/hands.obj");
Man[robot.RightSword]->readObj("./assets/Man/Sword.obj");
Man[robot.LeftSword]->readObj("./assets/Man/Sword.obj");
Man[robot.Head]->readObj("./assets/Man/Head.obj");
painter->bindObjectAndData(Man[robot.Body], BodyObject, "./assets/Man/Body.png", vshader, fshader);
painter->bindObjectAndData(Man[robot.LeftBigArm], LeftBigArmObject, "./assets/Man/BigArm.png", vshader, fshader);
painter->bindObjectAndData(Man[robot.RightBigArm], RightBigArmObject, "./assets/Man/BigArm.png", vshader, fshader);
painter->bindObjectAndData(Man[robot.LeftSmallArm], LeftSmallArmObject, "./assets/Man/SmallArm.png", vshader, fshader);
painter->bindObjectAndData(Man[robot.RightSmallArm], RightSmallArmObject, "./assets/Man/SmallArm.png", vshader, fshader);
painter->bindObjectAndData(Man[robot.RightHand], RightHandObject, "./assets/Man/hands.png", vshader, fshader);
painter->bindObjectAndData(Man[robot.LeftHand], LeftHandObject, "./assets/Man/hands.png", vshader, fshader);
painter->bindObjectAndData(Man[robot.RightSword], RightSwordObject, "./assets/Man/Sword.png", vshader, fshader);
painter->bindObjectAndData(Man[robot.LeftSword], LeftSwordObject, "./assets/Man/Sword.png", vshader, fshader);
painter->bindObjectAndData(Man[robot.Head], HeadObject, "./assets/Man/Head.png", vshader, fshader);
首先是定义Man中的TriMesh,对其进行初始化,然后进行对文件的读取,最后将文件与物体进行一个绑定。
在display函数中,是对物体进行拼接的地方,在此我写了一个drawMulMesh函数,将物体进行拼接。
首先渲染身体,这是机器人的中心部分。
// 躯干(这里我们希望机器人的躯干只绕Y轴旋转,所以只计算了RotateY)
if (ManModeFlag == true)
{
//当跟随模式开启,躯干的位置就是相机的位置,并且若头部和身体相差30度时,身体也跟随一起转动
modelMatrix = glm::translate(modelMatrix, glm::vec3(camera->eyex, 0.0, camera->eyez));
if (robot.theta[robot.Head] - robot.theta[robot.Body] > 30)
robot.theta[robot.Body] = robot.theta[robot.Head] - 30;
if (robot.theta[robot.Head] - robot.theta[robot.Body] < -30)
robot.theta[robot.Body] = robot.theta[robot.Head] + 30;
}
else
modelMatrix = glm::translate(modelMatrix, glm::vec3(0.0, 0.0, 0.0));
modelMatrix = glm::rotate(modelMatrix, glm::radians(robot.theta[robot.Body]), glm::vec3(0.0, 1.0, 0.0));
painter->drawMesh(Man[robot.Body], BodyObject, light, camera, body(modelMatrix));
mstack.push(modelMatrix); // 保存躯干变换矩阵
首先这个modelMatrix代表了这个身体物体的变换矩阵,可以通过这个改变ModelMatrix来改变身体部件。
//以下是层级建模用于控制关节点到物体的距离
glm::mat4 body(glm::mat4 modelMatrix)
{
// 本节点局部变换矩阵
glm::mat4 instance = glm::mat4(1.0);
instance = glm::translate(instance, glm::vec3(0.0, robot.BODY_HEIGHT * 0.5, 0.0));
instance = glm::scale(instance, glm::vec3(robot.BODY_WIDTH, robot.BODY_HEIGHT, robot.BODY_WIDTH));
return modelMatrix * instance;
}
接下来的body函数是在原本modelMatrix的基础上,对身体进行一个变换。然后将物体调用painter的drawMesh进行渲染。
再将本身身体的modelMatrix放入栈中,方便返回。然后对头部进行设计。
// 头部(这里我们希望机器人的头部只绕Y轴旋转,所以只计算了RotateY)
modelMatrix = glm::translate(modelMatrix, glm::vec3(0.0, robot.BODY_HEIGHT, 0.0));
if (ManModeFlag == true)
{
//当启动跟随模式,头部转动一直与相机欧拉角转动一致
robot.theta[robot.Head] = -camera->yaw;
modelMatrix = glm::rotate(modelMatrix, glm::radians(robot.theta[robot.Head] - robot.theta[robot.Body]), glm::vec3(0.0, 1.0, 0.0));
modelMatrix = glm::rotate(modelMatrix, glm::radians(-camera->pitch), glm::vec3(1.0, 0.0, 0.0));
}
else
modelMatrix = glm::rotate(modelMatrix, glm::radians(robot.theta[robot.Head]), glm::vec3(0.0, 1.0, 0.0));
painter->drawMesh(Man[robot.Head], HeadObject, light, camera, head(modelMatrix));
modelMatrix = mstack.pop(); // 恢复躯干变换矩阵
在此是设置头部与身体处的连接位置,也就是若要进行旋转,则在该位置进行旋转,并且通过rorate函数进行旋转。然后再到head函数当中,设置关节节点与物体的位置,使得完美的契合。
glm::mat4 head(glm::mat4 modelMatrix)
{
// 本节点局部变换矩阵
glm::mat4 instance = glm::mat4(1.0);
instance = glm::translate(instance, glm::vec3(0.0, -0.2, 0.0));
instance = glm::scale(instance, glm::vec3(robot.HEAD_WIDTH, robot.HEAD_HEIGHT, robot.HEAD_WIDTH));
return modelMatrix * instance;
}
设置完毕头部后,因为头部下面没有任何的部位了,所以此时将modelMatrix恢复成身体的样式,再进行接下来操作。
接下来的操作与上述大体相同,当某个部位下方没有任何部位后,则将栈中的变换矩阵返回,再进行后续操作。这样子通过栈可以实现层级建模。最终效果如下:
添加动画,可以根据display一直在运行的特性进行操作。首先定义一个TIME,当每运行一次display时将time进行加1,此时就可以获得一个相对时间的参数,我们可以通过这个相对时间的变换对物体制作动画。
float Fun = cos(TIME / Scale) * 90 - 90;
// =========== 左臂 ===========
mstack.push(modelMatrix); // 保存躯干变换矩阵
// 左大臂
Fun = cos(TIME / Scale) * 90 - 90;
modelMatrix = glm::translate(modelMatrix, glm::vec3(-0.43 * robot.BODY_WIDTH, 1.7, 0.0));
modelMatrix = glm::rotate(modelMatrix, glm::radians(robot.theta[robot.LeftBigArm] + Fun), glm::vec3(0.0, 0.0, 1.0));
painter->drawMesh(Man[robot.LeftBigArm], LeftBigArmObject, light, camera, Big_Arm(modelMatrix));
//左小臂
Fun = cos(TIME / Scale) * 45 - 45;
modelMatrix = glm::translate(modelMatrix, glm::vec3(0.0, -robot.BIG_ARM_HEIGHT * 0.35, 0.0));
modelMatrix = glm::rotate(modelMatrix, glm::radians(robot.theta[robot.LeftSmallArm] - Fun), glm::vec3(1.0, 0.0, 0.0));
painter->drawMesh(Man[robot.LeftSmallArm], LeftSmallArmObject, light, camera, Small_Arm(modelMatrix));
这里我是使用TIME来实现层级建模模型手臂的动画,通过TIME和特定函数改变手臂的转动角度,从而实现手臂的运动。
视角移动是使用了FPS游戏当中常见的WASD按键进行控制,并且使用SHIFT和CONTROL键控制视角位置的高度。
void Camera::keyboard(int key, int action, int mode)
{
// 键盘事件处理
// 通过按键改变相机和投影的参数
//通过计算,可以使的一直以相机的位置进行移动
if (key == GLFW_KEY_A && action == GLFW_PRESS && mode == 0x0000 || key == GLFW_KEY_A && action == GLFW_REPEAT && mode == 0x0000)
{
eyez += cos((yaw - 90) / 180 * PI) * 0.1;
eyex -= sin((yaw - 90) / 180 * PI) * 0.1;
}
else if (key == GLFW_KEY_D && action == GLFW_PRESS && mode == 0x0000 || key == GLFW_KEY_D && action == GLFW_REPEAT && mode == 0x0000)
{
eyez += cos((yaw + 90) / 180 * PI) * 0.1;
eyex -= sin((yaw + 90) / 180 * PI) * 0.1;
}
else if (key == GLFW_KEY_W && action == GLFW_PRESS && mode == 0x0000 || key == GLFW_KEY_W && action == GLFW_REPEAT && mode == 0x0000)
{
eyez += cos(yaw / 180 * PI) * 0.1;
eyex -= sin(yaw / 180 * PI) * 0.1;
}
else if (key == GLFW_KEY_S && action == GLFW_PRESS && mode == 0x0000 || key == GLFW_KEY_S && action == GLFW_REPEAT && mode == 0x0000)
{
eyez -= cos(yaw / 180 * PI) * 0.1;
eyex += sin(yaw / 180 * PI) * 0.1;
}
else if (key == GLFW_KEY_LEFT_SHIFT && action == GLFW_PRESS && mode == GLFW_MOD_SHIFT || key == GLFW_KEY_LEFT_SHIFT && action == GLFW_REPEAT && mode == GLFW_MOD_SHIFT)
{
eyey += 0.1;
}
else if (key == GLFW_KEY_LEFT_CONTROL && action == GLFW_PRESS && mode == GLFW_MOD_CONTROL || key == GLFW_KEY_LEFT_CONTROL && action == GLFW_REPEAT && mode == GLFW_MOD_CONTROL)
{
const float AEdis = 2.5;
if (eyey > (AEdis * sin(pitch * PI / 180)) && eyey > 0)
eyey -= 0.1;
}
else if (key == GLFW_KEY_SPACE && action == GLFW_PRESS && mode == 0x0000 || key == GLFW_KEY_SPACE && action == GLFW_REPEAT && mode == 0x0000)
{
radius = 4.0;
rotateAngle = 0.0;
upAngle = 0.0;
fov = 45.0;
aspect = 1.0;
scale = 1.5;
eyex = radius * cos(upAngle * M_PI / 180.0) * sin(rotateAngle * M_PI / 180.0);
eyez = radius * cos(upAngle * M_PI / 180.0) * cos(rotateAngle * M_PI / 180.0);
eyey = radius * sin(upAngle * M_PI / 180.0);
eyey += 1;
yaw = 179;
pitch = -15;
}
}
跟据公式可以计算出来根据yaw轴角度使得按下W键使得总是以视角前进方向,同理得到ASD键。并且在进行下降时,根据计算若pitch轴处于一定角度时,不能再进行下降,避免视角处于地面以下。
未加角度限制
增加角度限制
关于其余欧拉角的设置时使用的鼠标交互。通过查阅文档可知可以通过glfwSetCursorPosCallback函数隐藏鼠标并且当鼠标移动时获取鼠标的相对位置。
//隐藏鼠标并且随着鼠标移动改变相机视角
float lastX = 400, lastY = 300;
void mouse_callback(GLFWwindow *window, double xpos, double ypos)
{
//获取上一帧与下一帧鼠标位置差
float xoffset = xpos - lastX;
float yoffset = lastY - ypos;
lastX = xpos;
lastY = ypos;
//鼠标灵敏度
float sensitivity = 0.05f;
xoffset *= sensitivity;
yoffset *= sensitivity;
camera->mouse(xoffset, yoffset);
}
在mouse_callback当中,使用上一帧和此帧的坐标差,来进行输入,这样子就可以做到FPS这样的视角了,同时加上sensitivity,代表的是灵敏度。
使用mouse_button_callback函数对鼠标按键进行交互。在此左键是用于添加物体。
void mouse_button_callback(GLFWwindow *window, int button, int action, int mods)
{
//左键添加物体
if ((button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_PRESS) || (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_REPEAT))
{
//当处于仰角时不能添加物体
if (camera->pitch >= 0)
{
std::cout << "You can't add object at this camera position" << std::endl;
}
else
{
//获取光标位置,并进行整数化
std::vector meshes = painter->getMeshes();
glm::vec3 position = camera->getCenter(0.0);
position.x = (int)position.x;
position.y = (int)position.y;
position.z = (int)position.z;
int temp = meshes.size();
//判断是否可以放置
if (FillUp(position, glm::vec3(1.0), temp))
{
//添加物体并且选择该物体进行移动
addMeshes(position, glm::vec3(0.0, 0.0, 0.0), true, ItemName[ItemIndex], "./assets/item/" + ItemName[ItemIndex] + ".obj",
"./assets/item/" + ItemName[ItemIndex] + ".png", glm::vec3(1.65));
meshes = painter->getMeshes();
MeshIndex = meshes.size() - 1;
mesh = meshes[MeshIndex];
}
}
}
//右键对光标处的物体进行删除
if ((button == GLFW_MOUSE_BUTTON_RIGHT && action == GLFW_PRESS) || (button == GLFW_MOUSE_BUTTON_RIGHT && action == GLFW_REPEAT))
{
//获取光标位置,并进行整数化
glm::vec3 position = camera->getCenter(0.0);
position.x = (int)position.x;
position.y = (int)position.y;
position.z = (int)position.z;
int index = DeleteFill(position); //判断是否能删除
if (index != 0)
{
painter->deleteMesh(index);
}
MeshIndex--;
}
}
首先判断视角是否是仰角,是仰角将不能添加物体。不是仰角时,先获取相机视角下的中心点,此操作是通过getCenter函数进行,getCenter函数如下。
glm::vec3 Camera::getCenter(float y)
{
//获取视角面对的中心位置,参数y为获取y轴哪个高度的坐标。
float dis = eyey / tan(pitch / 180 * PI);
return glm::vec3(eyex + sin(yaw / 180 * PI) * dis, y, eyez - cos(yaw / 180 * PI) * dis);
}
首先是通过tan函数获取视角线在y平面处的投影,再在该平面上从而求出x坐标和z坐标。
获取完毕视角正对的坐标后,将视角进行整数化,因为我们希望物体放置在一个个整数坐标下。接下来用FillUp函数查看是否能够放置坐标,并且更新每个物体的位置。最后添加物体,并且将选择移动的物体变成该物体。
//右键对光标处的物体进行删除
if ((button == GLFW_MOUSE_BUTTON_RIGHT && action == GLFW_PRESS) || (button == GLFW_MOUSE_BUTTON_RIGHT && action == GLFW_REPEAT))
{
//获取光标位置,并进行整数化
glm::vec3 position = camera->getCenter(0.0);
position.x = (int)position.x;
position.y = (int)position.y;
position.z = (int)position.z;
int index = DeleteFill(position); //判断是否能删除
if (index != 0)
{
painter->deleteMesh(index);
}
MeshIndex--;
}
右键是删除指向的物体。同理获取坐标并且整数化,然后判断是否能进行删除操作。通过DeleteFill进行获取,并且删除该坐标的物体。
按下数字键1、2键可以选择需要移动的物体。
//若按下1键,控制的是上一个物体,按下的是2键,控制的是下一个物体
case GLFW_KEY_1:
MeshIndex--;
if (MeshIndex <= 1)
MeshIndex = 2;
mesh = meshes[MeshIndex];
meshes.clear();
break;
case GLFW_KEY_2:
MeshIndex++;
if (MeshIndex > meshes.size() - 1)
MeshIndex = meshes.size();
mesh = meshes[MeshIndex];
break;
该操作是通过改变全局指针mesh进行操作,当发生改变时,首先判断是否能进行下一个或上一个物体,并且将相应的物体地址给到mesh当中。
case GLFW_KEY_UP:
if (changeFill(mesh->getTranslation(), mesh->getTranslation() + glm::vec3(0, 0, -1)))
mesh->updateTranslation(2, -1);
break;
case GLFW_KEY_DOWN:
if (changeFill(mesh->getTranslation(), mesh->getTranslation() + glm::vec3(0, 0, 1)))
mesh->updateTranslation(2, 1);
break;
case GLFW_KEY_LEFT:
if (changeFill(mesh->getTranslation(), mesh->getTranslation() + glm::vec3(-1, 0, 0)))
mesh->updateTranslation(0, -1);
break;
case GLFW_KEY_RIGHT:
if (changeFill(mesh->getTranslation(), mesh->getTranslation() + glm::vec3(1, 0, 0)))
mesh->updateTranslation(0, 1);
break;
移动物体的操作通过使用updateTranslation进行操作,并且首先调用changeFill。
//选择需要添加的物体
case GLFW_KEY_Q:
if (ItemIndex > 0)
ItemIndex--;
std::cout << "Slected Add Object is: " << ItemName[ItemIndex] << std::endl;
break;
case GLFW_KEY_E:
if (ItemIndex < 10)
ItemIndex++;
std::cout << "Slected Add Object is: " << ItemName[ItemIndex] << std::endl;
break;
Q和E键可以改变添加的物体,原理与上述差不多,通过改变ItemIndex进行实现。
可以通过使用UJIKOL六个按键对光源的位置进行调整。
//更改灯光位置
case GLFW_KEY_U:
pos = light->getTranslation();
pos.x += 0.5;
light->setTranslation(pos);
std::cout << "Light Position:" << pos.x << " " << pos.y << " " << pos.z << std::endl;
break;
case GLFW_KEY_J:
pos = light->getTranslation();
pos.x -= 0.5;
light->setTranslation(pos);
std::cout << "Light Position:" << pos.x << " " << pos.y << " " << pos.z << std::endl;
break;
case GLFW_KEY_I:
pos = light->getTranslation();
pos.y += 0.5;
light->setTranslation(pos);
std::cout << "Light Position:" << pos.x << " " << pos.y << " " << pos.z << std::endl;
break;
case GLFW_KEY_K:
pos = light->getTranslation();
if (pos.y >= 20)
pos.y -= 0.5;
light->setTranslation(pos);
std::cout << "Light Position:" << pos.x << " " << pos.y << " " << pos.z << std::endl;
break;
case GLFW_KEY_O:
pos = light->getTranslation();
pos.z += 0.5;
light->setTranslation(pos);
std::cout << "Light Position:" << pos.x << " " << pos.y << " " << pos.z << std::endl;
break;
case GLFW_KEY_L:
pos = light->getTranslation();
pos.z -= 0.5;
light->setTranslation(pos);
std::cout << "Light Position:" << pos.x << " " << pos.y << " " << pos.z << std::endl;
break;
使用light的setTranslation进行实现,并且在y方向上光源不得低于20。
按下小键盘Enter键可以使得层级建模模型进行移动,按下小键盘数组键可以选择需要转动的部位,并且通过按键z和x控制转动角度。
//移动机器人
case GLFW_KEY_KP_ENTER:
if (ManModeFlag == 0)
mesh = Man[robot.Body];
std::cout << "Select Moving Object is Man" << std::endl;
break;
//选择旋转机器人部件
case GLFW_KEY_KP_0:
Selected_mesh = robot.Body;
break;
case GLFW_KEY_KP_1:
Selected_mesh = robot.Head;
break;
case GLFW_KEY_KP_2:
Selected_mesh = robot.RightBigArm;
break;
case GLFW_KEY_KP_3:
Selected_mesh = robot.LeftBigArm;
break;
case GLFW_KEY_KP_4:
Selected_mesh = robot.RightSmallArm;
break;
case GLFW_KEY_KP_5:
Selected_mesh = robot.LeftSmallArm;
break;
case GLFW_KEY_KP_6:
Selected_mesh = robot.RightHand;
break;
case GLFW_KEY_KP_7:
Selected_mesh = robot.LeftHand;
break;
case GLFW_KEY_KP_8:
Selected_mesh = robot.RightSword;
break;
case GLFW_KEY_KP_9:
Selected_mesh = robot.LeftSword;
break;
// 通过按键旋转
case GLFW_KEY_Z:
robot.theta[Selected_mesh] += 5.0;
if (robot.theta[Selected_mesh] > 360.0)
robot.theta[Selected_mesh] -= 360.0;
break;
case GLFW_KEY_X:
robot.theta[Selected_mesh] -= 5.0;
if (robot.theta[Selected_mesh] < 0.0)
robot.theta[Selected_mesh] += 360.0;
break;
按下按键M可以进入机器人视角跟随模式。
//启动相机跟随物体模式第二次按下取消
case GLFW_KEY_M:
if (ManModeFlag == 0)
{
std::cout << "Enable Man Moving Mode" << std::endl;
ManModeFlag = 1;
mesh = meshes[MeshIndex]; //移动物体恢复成移动其他物体
//将相机移动到机器人头部的视角处
camera->eyex = Man[robot.Body]->getTranslation().x;
camera->eyey = Man[robot.Body]->getTranslation().y + 2;
camera->eyez = Man[robot.Body]->getTranslation().z;
camera->yaw = -robot.theta[robot.Head];
camera->pitch = Man[robot.Head]->getRotation().x;
Man[robot.Body]->setTranslation(glm::vec3(0.0, 0.0, 0.0));
//这个将物体归0是因为执行以后,相机坐标移动到机器人处,然后机器人处又会根据相机进一步移动,所以先归0
}
else
{
std::cout << "Disable Man Moving Mode" << std::endl;
ManModeFlag = 0;
robot.theta[robot.Head] = robot.theta[robot.Head] - robot.theta[robot.Body];
Man[robot.Body]->setTranslation(glm::vec3(camera->eyex, 0.0, camera->eyez));
//保存机器人当前位置
}
break;
这里是使用ManModeFlag来进行控制,当按下时,将视角转移到机器人头部处,并且视角的pitch轴和yaw轴角度与机器人头部的x轴和y轴相同,并且设置ManModeFlag为1。
关闭时,将ManModeFlag设置为0,并且保存机器人当前位置。
在DrawMulMesh当中,当开启视角跟随模式后,会将视角与机器人的身体和头部进行绑定。
if (ManModeFlag == true)
{
//当跟随模式开启,躯干的位置就是相机的位置,并且若头部和身体相差30度时,身体也跟随一起转动
modelMatrix = glm::translate(modelMatrix, glm::vec3(camera->eyex, 0.0, camera->eyez));
if (robot.theta[robot.Head] - robot.theta[robot.Body] > 30)
robot.theta[robot.Body] = robot.theta[robot.Head] - 30;
if (robot.theta[robot.Head] - robot.theta[robot.Body] < -30)
robot.theta[robot.Body] = robot.theta[robot.Head] + 30;
}
else
modelMatrix = glm::translate(modelMatrix, glm::vec3(0.0, 0.0, 0.0));
首先,身体部位是在头部与身体产生大于30度偏角时,进行旋转。并且身体的位置与相机进行绑定。
头部是与相机的pitch轴和yaw轴进行绑定,这样子机器人就可以随时跟随相机视角了。
在写相机跟随模式时,我发现头部是一直可以固定在中间位置的,从而有了启发,可以像头部一样将光标与相机绑定,这样就可以做出一个类似的光标。
与之前原理相同,将物体绑定在painter当中,同时也是与Ground一样不使用阴影。接着在display函数当中将物体与视角进行绑定。
glm::mat4 modelMatrix = Cross->getModelMatrix();
modelMatrix = glm::translate(modelMatrix, glm::vec3(camera->eyex, camera->eyey, camera->eyez));
modelMatrix = glm::rotate(modelMatrix, glm::radians(-camera->yaw), glm::vec3(0.0, 1.0, 0.0));
modelMatrix = glm::rotate(modelMatrix, glm::radians(-camera->pitch), glm::vec3(1.0, 0.0, 0.0));
painter->drawMesh(Cross, CrossObject, light, camera, modelMatrix, 0);