一、关于ViewCube
三维场景中如果没有提供一个有效的方案来供用户作为方向的参考,那么很容易在使用的过程中就散失对位置的判断,因此许多建模软件会在右上角增加一个ViewCube来进行方位辅助,在本应用中也实现一个类似的ViewCube。
二、ViewCube需要实现的功能
- Cube显示在绘图区右上角位置,盒子大小随着窗口的改变而改变,当大到一定程度是将不再增大;
- Cube六个面中心处贴上相应的图片方便辨别方向;
三、ViewCube的实现方案
实现方案1:
绘制一个立方体,立方体每个面进行贴图,考虑到后续捕捉问题,需要对每个面准备两张贴图,一张是需要显示的图片,另一张是含这个面各个部分编码的图片,如下图所示:
以“前”这个面为例,绘制这个面需要4个顶点,并为其准备两张贴图,中间的图片是渲染后需要显示的画面,右侧图片中的1-9数字是指图片上每个像素的值,在渲染时就能记录下“前”这个面上每个像素代表的是哪个部位,那么在接下来鼠标点击选择时能够快速且准确的确定相应的部分并作出视角切换。以此类推,立方体的每个面都是如此处理,当然,每个面的编码根据具体情况而定。
实现方案2:
绘制一个立方体,将立方体的每个面分为9个部分,如方案1中的右侧编码图所示的9个部分,此时对每个面的编码值直接记录在对应的顶点数据中即可,最后准备一张和方案1所示中间位置的贴图,仅对编码为5的区域进行贴图。
具体实现:
本次选择方案2的方式实现,主要是考虑将立方体内的顶点数量增多后在后期如果碰到有特别的需要好方便扩展,并且编码直接通过代码填写比将其转化成编码贴图要简易。实现过程需要准备的数据如下:
- 顶点数据
struct VertexDataID_1 //顶点数据结构
{
QVector3D m_Vert; //坐标
QVector3D m_Normal; //法向
QVector2D m_Texture; //贴图坐标
uint32_t m_ID; //顶点编码值
}
- 贴图数据
制作六张“前”、“后”、“上”、“下”、“左”、“右”的简易图片,并添加到qrc中供后续使用,在准备贴图数据的过程如下:
void MLINVulkanPrivate::createViewCubeTextureRes()
{
QImage img_Front(":/ DirBox/Images/Front.png");//0
img_Front = img_Front.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
QImage img_Back, img_Down, img_Up, img_Left, img_Right;
...//图片处理同img_Front,此处省略具体代码
QSize imageMeasurement = img_Front.size();//每张图片的像素尺寸
createImage(imageMeasurement.width(), imageMeasurement.height(), VK_SAMPLE_COUNT_1_BIT,
VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL,
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, mViewCubeTexture.mImage,
mViewCubeTexture.mImageMemory, 6);//创建用于贴图的图片
VkDeviceSize gingleImageSize = img_Front.sizeInBytes();//单张图片的数据大小
VkBuffer stagingBuffer;
VkDeviceMemory stagingBufferMemory;
createBuffer(gingleImageSize * 6, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
stagingBuffer, stagingBufferMemory);//先将图片数据拷贝到buffer中
char *pChar;
vkMapMemory(mDevice, stagingBufferMemory, 0, gingleImageSize * 6, 0,
reinterpret_cast<void**>(&pChar));
memcpy(pChar, img_Front.bits(), gingleImageSize);
memcpy(pChar + gingleImageSize, img_Back.bits(), gingleImageSize);
memcpy(pChar + gingleImageSize * 2, img_Up.bits(), gingleImageSize);
memcpy(pChar + gingleImageSize * 3, img_Down.bits(), gingleImageSize);
memcpy(pChar + gingleImageSize * 4, img_Left.bits(), gingleImageSize);
memcpy(pChar + gingleImageSize * 5, img_Right.bits(), gingleImageSize);
vkUnmapMemory(mDevice, stagingBufferMemory);
//将图片布局由VK_IMAGE_LAYOUT_UNDEFINED转成VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
transitionImageLayout(mViewCubeTexture.mImage, VK_FORMAT_R8G8B8A8_SRGB,
VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 6);
QVector<VkBufferImageCopy> regions = {
VkBufferImageCopy
{
0, //VkDeviceSize bufferOffset;
0, //uint32_t bufferRowLength;
0, //uint32_t bufferImageHeight;
VkImageSubresourceLayers //VkImageSubresourceLayers imageSubresource;
{
VK_IMAGE_ASPECT_COLOR_BIT, //VkImageAspectFlags aspectMask;
0, //uint32_t mipLevel 由于ViewCube的大小在画面中相对固定,因此没必要采用mipmap
0, //uint32_t baseArrayLayer;
6 //uint32_t layerCount; //将所有图片放入数组中因此此处为6
},
VkOffset3D{0, 0, 0}, //VkOffset3D imageOffset;
VkExtent3D
{
static_cast<uint32_t>(imageMeasurement.width()),
static_cast<uint32_t>(imageMeasurement.height()),
1
} //VkExtent3D imageExtent;
}
};
copyBufferToImage(stagingBuffer, mViewCubeTexture.mImage, regions);//拷贝数据到imageMemory中
//将图片布局由VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL转成VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
transitionImageLayout(mViewCubeTexture.mImage, VK_FORMAT_R8G8B8A8_SRGB,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, 6);
vkDestroyBuffer(mDevice, stagingBuffer, nullptr);
vkFreeMemory(mDevice, stagingBufferMemory, nullptr);
//创建image对应的view
mViewCubeTexture.mImageView = createImageView(mViewCubeTexture.mImage,
VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_ASPECT_COLOR_BIT, VK_IMAGE_VIEW_TYPE_2D_ARRAY, 6);
}
上述代码中需要注意一些事项:
a、创建图片的时候是采用最优瓦片组织方式(VK_IMAGE_TILING_OPTIMAL),因此需要通过buffer来作为中间传递。
b、创建图片的时初始布局为VK_IMAGE_LAYOUT_UNDEFINED,在从buffer拷贝数据到图片的过程需要将其转化为VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;当拷贝结束后图片将要作为贴图数据,因此需要将其转化为VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,上面贴出的代码将初次转化布局、拷贝数据、再次转化布局这三个步骤分成了三个命令,这仅是在代码中好区分这个过程,正常是合并为一个command。
c、采用的时候六个图片合并成了一个数组,因此在创建视图的时候类型应该为
VK_IMAGE_VIEW_TYPE_2D_ARRAY
- 渲染管线
a、CubeView始终显示在屏幕上并且在所有图形之上,因此可以考虑在最后来绘制图形并且始终覆盖其他图形,那么在设置管线的深度状态信息时(VkPipelineDepthStencilStateCreateInfo),depthTestEnable、depthWriteEnable均为VK_TRUE,depthCompareOp选择VK_COMPARE_OP_ALWAYS。
b、整个CubeView为密封的立方体,无需绘制反面表面,在光栅化状态信息(VkPipelineRasterizationStateCreateInfo)中可将反面剔除,即cullMode = VK_CULL_MODE_BACK_BIT。 - 着色器
整个立方体分为两部分绘制,一部分是贴图区域,另外是非贴图区域,下面代码记录贴图部分的着色器,而非贴图区域将贴图部分去除即可
a、顶点着色器
#version 440
layout(location = 0) in vec3 i_pos; //坐标
layout(location = 1) in vec3 i_nor; //法向
layout(location = 2) in vec2 i_Texture; //贴图
layout(location = 3) in uint i_EntID; //实体ID
const vec3 c_diffuse = vec3(0.8, 0.8, 0.8); //物体散射光强度系数
layout(location = 0) out vec3 o_color; //计算得到的最终颜色
layout(location = 1) out vec4 o_position;//计算得到的最终坐标
layout(location = 2) out vec3 o_texture;//贴图坐标
layout(std140, binding = 0) uniform UVal {
mat4 u_MVP; //MVP变化矩阵(ViewCube渲染可忽略)
mat4 u_Module; //物体的位置修正矩阵(ViewCube渲染可忽略)
vec3 u_LightDir; //光线实际方向(ViewCube渲染可忽略)
vec3 u_Ambient; //环境光强度系数
vec3 u_CameraPos; //相机位置
vec3 u_CameraFocus; //相机焦点
} uVal;
layout(push_constant) uniform PushConsts {
mat4 cTransform;
} pushConsts;
void main()
{
vec3 lightDIr = normalize(uVal.u_CameraPos - uVal.u_CameraFocus);//光线始终与视线同向
gl_Position = pushConsts.cTransform * vec4(i_pos, 1.0);//计算图形在图面的位置,变化矩阵在主程序中计算传入
o_position = vec4(gl_Position.xyz, i_EntID);//此处最重要是记录各个面的子部分ID
o_texture = vec3(i_Texture, i_EntID % 10);//%10是根据带贴图所在面的实际编码调整
vec3 diffuseFactor = max(0.0, dot(lightDIr, i_nor)) * c_diffuse;//散射光强度
o_color = (1 + diffuseFactor) * uVal.u_Ambient;
}
b、片段着色器
#version 440
layout(location = 0) in vec3 i_color; //计算得到的最终颜色
layout(location = 1) in vec4 i_position;//计算得到的最终颜色
layout(location = 2) in vec3 i_texture;//贴图位置
layout(location = 0) out vec4 fragColor;
layout(location = 1) out vec4 fragCoordinate;
layout(binding = 0, set = 1) uniform sampler2DArray tex;
void main()
{
fragColor = vec4(i_color * (textureLod(tex, i_texture, 0.0).xyz), 0.8);
fragCoordinate = i_position;
}
- 关于顶点着色器推送常量中矩阵的计算
//! 描述: 计算ViewCube的显示变化矩阵
//! 参数 viewDirection:视线方向
//! 参数 up:相机向上方向
//! 参数 winWidth: 渲染区域像素宽
//! cans winHeight: 渲染区域像素高
void MLINDirectionBox::CalTransform(const QVector3D& viewDirection, const QVector3D& up, float winWidth, float winHeight)
{
QVector3D focus(0, 0, 0);
QVector3D eye = focus - viewDirection;
QMatrix4x4 viewMat;
viewMat.lookAt(eye, focus, up);
MLINAxisAlignedBoundingBox tempBox = mBox;
tempBox.world2View(viewMat);
float boxSize = tempBox.getDeep();
float tempSize = tempBox.getHeight();
boxSize = boxSize >= tempSize ? boxSize : tempSize;
tempSize = tempBox.getWidth();
boxSize = boxSize >= tempSize ? boxSize : tempSize;
float mGraphicSize = winWidth * 0.5 * 0.15;
if (mGraphicSize >= 55)//限制ViewCube的最大尺寸
mGraphicSize = 55;
float graphicScale = mGraphicSize / (winWidth * 0.5);//注意此处以宽度为基准,原因是本程序渲染宽高比是以宽度为基准确认
float boxScale = boxSize / 2;
QMatrix4x4 scaleMat;
scaleMat.scale(boxScale * graphicScale);//确定ViewCube的模型缩放矩阵
float projWidth = boxSize;
float winRatio = winWidth / winHeight;
float projHeight = projWidth / winRatio;
float nearPlane = -boxSize;
float farPlane = boxSize;
projWidth *= 0.5;
projHeight *= 0.5;
QMatrix4x4 orthoMat;
orthoMat.ortho(-projWidth, projWidth, -projHeight, projHeight, nearPlane, farPlane);
//计算Cube向左的偏移量
float movePixel = mGraphicSize * 1.6;//偏移量根据实际绘制的像素大小决定
float moveLeft = 2 * float(movePixel) / winWidth;
if (moveLeft <= 1)
moveLeft = 1 - moveLeft;
else
moveLeft = moveLeft - 1;
float moveBottom = 2 * float(movePixel) / winHeight;
if (moveBottom <= 1)
moveBottom = 1 - moveBottom;
else
moveBottom = moveBottom - 1;
QMatrix4x4 moveMat;
moveMat.translate(QVector3D(moveLeft, moveBottom, 0));
mTransform = VulkanMath::clipMat * moveMat * scaleMat * orthoMat * viewMat;
}
以上代码计算过程:将以坐标原点绘制的Cube投影至标准坐标系中即 orthoMat * viewMat 的过程,然后根据渲染区域的宽高确定图形的相对大小及相对位置即 moveMat * scaleMat 。
四、运行效果
a、鼠标不在ViewCube上的效果
b、鼠标选择了其中一个面的效果,如向上一面高亮显蓝