案例需求:
在三维超声显示中,需要一个光源指示功能来示意光源是从什么方向照向胎儿的,从而帮助用户去理解当前胎儿三维显示的效果。如下图
基于以上需求需要实现以下几点功能:
1. 构造球体模型和光源模型;
2. 绕球体旋转光源;
3. 渲染光源和球体任意位置关系。
案例实现
胎儿用一个球体示意,光源用一个立方体示意。因为立方体的构造原理很简单,所以这里只对球体的构造原理进行说明。
构造球体模型
球是由一组经线和纬线上的点组成,如下图所示:
图1
计算球上点的坐标
下面以球的模型坐标系来计算球上任意一点P的坐标(xpos, ypos, zpos),如下图:
为了简化分析图,把坐标系和P点从球上拿出来,如下图:
为了进一步简化分析,把P点及其在坐标系中各个轴上的投影点一起构造一个立方体,如下图:
θ :表示经线与X轴正方向的角
φ :表示纬线与Y轴正方向的角
半径:单位长度,即是图中OP的长度为1.
从图中可以看出:
∠poq = ∠soz = φ
所以P点的坐标计算如下:
xpos = cos(φ) * sin(θ)
ypos = sin(φ)
zpos = cos(φ) * cos(θ)
我们可以根据需要设置经线和纬线的条数(太少的话,球会有棱有角,不够光滑):
LONGITUDE_SEGMENTS = 64
LATITUDE_SEGMENTS = 64
则θ和φ的计算如下:
x,y∈[0, 64]
θ = (x / LONGITUDE_SEGMENTS) * 2 * π
φ = (y / LATITUDE_SEGMENTS) * 2 * π
从图1中我们看到纬线的角度范围实际是[-90, 90],所以φ的计算更正为:
φ = (π / 2) - (y / LATITUDE_SEGMENTS) * π
std::vector<glm::vec3>positions, normals;
const unsigned int LONGITUDE_SEGMENTS = 64;
const unsigned int LATITUDE_SEGMENTS = 64;
const float PI = 3.14159265359f;
for (unsigned int x = 0; x <= LONGITUDE_SEGMENTS; ++x)
{for (unsigned int y = 0; y <= LATITUDE_SEGMENTS; ++y){float theta = ((float)x / (float)LONGITUDE_SEGMENTS) * 2 * PI;float phi = (PI / 2) - ((float)y / (float)LATITUDE_SEGMENTS) * PI;float xPos = std::cos(phi) * std::sin(theta);float yPos = std::sin(phi);float zPos = std::cos(phi) * std::cos(theta);positions.push_back(xPos);positions.push_back(yPos);positions.push_back(zPos);normals.push_back(xPos);normals.push_back(yPos);normals.push_back(zPos);}
}
生成点的索引
我们将使用EBO的方式来绘制球,所以需要生成点的索引。下图是基于优先遍历纬线方向的索引点的示意图:
上图简化为一个的平面网格图如下:
indices[] = {0, 3, 1, 3, 4, 1, 1, 4, 2, 4, 5, 2, 3, 6, 4, 6, 7,4, 4, 7, 5, 7, 8, 5 }
if(x < LONGITUDE_SEGMENTS && y < LATITUDE_SEGMENTS)
{indices.push_back(x * (LATITUDE_SEGMENTS + 1) + y);indices.push_back((x + 1) * (LATITUDE_SEGMENTS + 1) + y);indices.push_back(x * (LATITUDE_SEGMENTS + 1) + y + 1);indices.push_back((x + 1) * (LATITUDE_SEGMENTS + 1) + y);indices.push_back(x * (LATITUDE_SEGMENTS + 1) + y + 1);indices.push_back((x + 1) * (LATITUDE_SEGMENTS + 1) + y + 1);
}
至此,球模型构造完成。
绕球体旋转光源
光源绕球体旋转是通过鼠标移动实现,所以需要计算鼠标屏幕偏移量到球上偏移量的计算:
这种计算的方法很多,我采用的是将鼠标偏移量转换为球的经线和纬线方向偏移角的方法,代码实现如下,原理与计算球上任意一点坐标的一致。
float light_theta = 0;
float light_phi = 0;
void MaptoSphere(glm::vec3& lightPos)
{const float PI = 3.14159265359f;float length = glm::distance(lightPos, glm::vec3(0, 0, 0));float theta = glm::radians(light_theta);float phi = glm::radians(light_phi);; float xPos = length * std::cos(phi) * std::sin(theta);float yPos = length * std::sin(phi);float zPos = length * std::cos(phi) * std::cos(theta);lightPos = glm::vec3(xPos, yPos, zPos);
}// glfw: whenever the mouse moves, this callback is called
// -------------------------------------------------------
void mouse_callback(GLFWwindow* window, double xposIn, double yposIn)
{float xpos = static_cast<float>(xposIn);float ypos = static_cast<float>(yposIn);if (firstMouse){lastX = xpos;lastY = ypos;firstMouse = false;}float xoffset = xpos - lastX;float yoffset = ypos - lastY; // reversed since y-coordinates go from bottom to top const float MouseSensitivity = 0.5f;xoffset *= MouseSensitivity;yoffset *= MouseSensitivity;light_theta += xoffset;light_phi += yoffset;lastX = xpos;lastY = ypos;if (fabs(xoffset) < 0.0001 && fabs(yoffset) < 0.0001){return;}MaptoSphere(lightPos);
}
渲染光源和球体任意位置关系
立方体光源的渲染比较简单,这里只对球体的渲染进行说明。
顶点着色器代码:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;out vec3 FragPos;
out vec3 Normal;uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;void main()
{FragPos = vec3(model * vec4(aPos, 1.0));Normal = mat3(transpose(inverse(model))) * aNormal; gl_Position = projection * view * vec4(FragPos, 1.0);
}
片段着色器代码:
#version 330 core
out vec4 FragColor;in vec3 Normal;
in vec3 FragPos; uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;
uniform vec3 objectColor;
uniform bool blinn;void main()
{// ambientfloat ambientStrength = 0.6;vec3 ambient = ambientStrength * lightColor;// diffuse vec3 norm = normalize(Normal);vec3 lightDir = normalize(lightPos - FragPos);float diff = max(dot(norm, lightDir), 0.0);vec3 diffuse = diff * lightColor;// specularfloat specularStrength = 0.2;float spec = 0.0;vec3 viewDir = normalize(viewPos - FragPos);if(blinn){vec3 halfwayDir = normalize(lightDir + viewDir);spec = pow(max(dot(Normal, halfwayDir), 0.0), 64);}else{vec3 reflectDir = reflect(-lightDir, norm); float spec = pow(max(dot(viewDir, reflectDir), 0.0), 64);} vec3 specular = specularStrength * spec * lightColor; vec3 result = (ambient + diffuse + specular) * objectColor;FragColor = vec4(result, 0.5);
}
FragColor = vec4(result, 0.5);
这里0.5表示透明度,不能设置为1.0。只有在透明的情况下,当光源转到球背后时,才依然能看到光源的位置。
混合和面剔除
...
while (!glfwWindowShouldClose(window))
{// render// ------glClearColor(0.1f, 0.1f, 0.1f, 1.0f);glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);glDisable(GL_BLEND);glDisable(GL_CULL_FACE);cube.Render();if (lightPos.z <= 0){glEnable(GL_BLEND);glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);glEnable(GL_CULL_FACE);}else{glDisable(GL_BLEND);glDisable(GL_CULL_FACE);}sphere.Render();// // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)// -------------------------------------------------------------------------------glfwSwapBuffers(window);glfwPollEvents();
}
...