案例需求:
在三维超声显示中,需要一个光源指示功能来示意光源是从什么方向照向胎儿的,从而帮助用户去理解当前胎儿三维显示的效果。如下图
基于以上需求需要实现以下几点功能:
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()
{
// ambient
float 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;
// specular
float 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();
}
...