VR渲染之Stereo Rendering解析

news2025/1/22 14:43:55

VR渲染的独特和最明显的方面之一是需要生成两个视图,左右眼睛各一个。我们需要这两个视图来为观众创建立体3D效果。

Multi Camera

传统上,VR应用程序必须绘制两次几何体--一次是左眼,一次是右眼。这基本上使非VR应用程序所需的处理翻了一番。

要显示每只眼睛的视图,最简单的方法是运行两次渲染循环。每个眼睛都将配置并经历自己的渲染循环迭代。最后,我们将有两个图像,我们可以发送到显示设备。底层实现使用两个Unity相机,每只眼睛一个,它们经历生成立体图像的过程。

虽然这种方法肯定有效,但多摄像头依赖于暴力,在CPU和GPU方面效率最低。CPU必须完全迭代两次渲染循环,GPU可能无法使用两次被拉到眼睛上的对象的缓存。

Multi Pass

Multi Pass是优化XR渲染循环的方法之一。核心思想是提取渲染循环中与视图无关的部分。这意味着,任何不明确依赖XR视图的工作都不必每只眼睛完成。

此优化最明显的的优化方向之一是阴影渲染。阴影并不明确独立于相机查看器的位置。Unity实际上通过两个步骤实现阴影:创建级联阴影贴图,然后将阴影分配给屏幕空间。对于Multi-Pass,我们可以创建一组级联阴影贴图,然后为屏幕创建两个阴影贴图,因为屏幕空间的阴影贴图取决于查看器的位置。由于我们的阴影生成的体系结构,屏幕空间的阴影贴图受益于局部性,因为阴影贴图生成循环耦合相对紧密。这可以与剩余的渲染工作流进行比较,后者需要在渲染循环上完成迭代,然后返回到类似的阶段(例如,眼睛特定的不透明过程被剩余的渲染循环阶段分开)。

另一个可以在两只眼睛之间分裂的步骤一开始可能并不明显:我们可以在两只眼睛之间做一次扑杀。在我们的第一个实现中,我们使用截头剔除来生成两个对象列表,每只眼睛一个。然而,我们可以创造一个均匀的扑杀脸,在我们的两只眼睛之间分开。这意味着每只眼睛都比单眼剔除截头要多一些,但我们已经考虑了单次排序的好处,以超过一些额外的顶点着色器、剪裁和筛选的成本。

Multi Pass比多摄像头节省了一些很好的费用,但还有更多的事情要做。

Multi Camera与Multi Pass虽然在性能上差了一些,但有一个好处就是在于不会对shader进行侵入式修改。

Single Pass

Single Pass渲染意味着我们对整个渲染循环进行渲染一次,能够提高性能。

要进行这两个绘制,我们需要确保我们有所有的常量数据和索引绑定。

渲染引擎怎样draw呢?在Multi Pass中,每个眼睛都有自己的渲染目标,但对于Single Pass,我们不能这样做,因为为连续的绘制调用切换渲染目标的成本也是极大的。一种优化思路是使用渲染目标数组,但我们必须从大多数平台上的几何着色器导出数组索引,这在GPU上可能很昂贵,对于现有着色器来说也是侵入性的。

当前的解决方案是使用双宽渲染目标,并在绘制调用之间切换视口,以便每只眼睛都可以渲染到双宽渲染目标的一半。更改视口成本也高昂,但比更改渲染目标更低复杂,也比使用几何着色器更低侵入性。还有使用视口数组的相关选项,但它们与渲染目标数组存在相同的问题,因为索引只能从几何着色器导出。还有另一种使用动态剪裁的技术,我们在这里就不讨论了。

现在,我们有了一个解决方案,可以开始连续两次draw,以看到两只眼睛,但是需要硬件上支持。使用Single-Pass,我们不希望在恒定缓冲区绑定之间不必要地切换。相反,我们将两只眼睛的视觉矩阵和投影矩阵联系在一起,并使用StereoEyeIndex索引它们。

另一个细节:要最大限度地减少Viewport和unity_StereoEyeIndex状态更改,我们可以更改眼睛绘制模式。我们可以画左、右、右、左等,而不是画左、右、左、右等。使用节奏。这允许我们将状态更新的数量与交替节奏相比减少一半。

性能这并不完全是Multi Pass的两倍。对剔除和阴影进行了优化,而且我们仍在通过眼睛发送绘制和切换视口,这导致了一些CPU和GPU成本。

在OpenGL中,使用glTexImage3DMultisample赋值双宽度纹理数组

glBindTexture(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, framebufferDesc.m_nRenderTextureId);
glTexImage3DMultisample(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, 4, GL_RGBA8, nWidth, nHeight, 2, GL_FALSE);

我们来看看OpenGL的具体实现方式: 

	struct FramebufferDescSPS
	{
		GLuint m_nDepthBufferId;
		GLuint m_nRenderTextureId;
		GLuint m_nRenderFramebufferId;
		GLuint m_nCombinedFramebufferId[2]; // framebuffer where we will render each layer separately
		GLuint m_nReadFramebufferId; // needed to bind the texture layers for the blit
		GLuint m_nResolveTextureId[2];
		GLuint m_nResolveFramebufferId[2];
	};

bool CreateFrameBufferSPS( int nWidth, int nHeight, FramebufferDescSPS &framebufferDesc )
{
	glGenFramebuffers(1, &framebufferDesc.m_nRenderFramebufferId );
	glBindFramebuffer(GL_FRAMEBUFFER, framebufferDesc.m_nRenderFramebufferId);

	glGenTextures(1, &framebufferDesc.m_nDepthBufferId);
	glBindTexture(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, framebufferDesc.m_nDepthBufferId);
	glTexImage3DMultisample(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, 4, GL_DEPTH_COMPONENT24, nWidth, nHeight, 2, GL_FALSE);
	glBindTexture(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, 0);

	glGenTextures(1, &framebufferDesc.m_nRenderTextureId);
	glBindTexture(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, framebufferDesc.m_nRenderTextureId);
	glTexImage3DMultisample(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, 4, GL_RGBA8, nWidth, nHeight, 2, GL_FALSE);
	glBindTexture(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, 0);

	// check FBO status
	GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
	if (status != GL_FRAMEBUFFER_COMPLETE)
	{
		return false;
	}

	glBindFramebuffer( GL_FRAMEBUFFER, 0 );

	return true;
}

shader中需要加入扩展,#extension GL_NV_viewport_array2: require

#extension GL_NV_stereo_view_rendering: require

我们传入的时候需要传入左右眼的MVP

	m_unSceneProgramIDSPS = CompileGLShader(
		"Scene",

		// Vertex Shader
		"#version 410\n"
		"#extension GL_ARB_shading_language_include : enable\n"
		"#extension GL_NV_viewport_array2: require\n"

		"#extension GL_NV_stereo_view_rendering: require\n"
		"layout(secondary_view_offset=1) out highp int gl_Layer;\n"

		"uniform mat4 matrixEyeLeft;\n"
		"uniform mat4 matrixEyeRight;\n"

		"layout(location = 0) in vec4 position;\n"
		"layout(location = 1) in vec2 v2UVcoordsIn;\n"
		"layout(location = 2) in vec3 v3NormalIn;\n"
		"out vec2 v2UVcoords;\n"
		"void main()\n"
		"{\n"
		"	v2UVcoords = v2UVcoordsIn;\n"
		"	gl_Position = matrixEyeLeft * position;\n"
		"	gl_SecondaryPositionNV = matrixEyeRight * position;\n"
		"	gl_Layer = 0;\n"

		"}\n",

		// Fragment Shader
		"#version 410 core\n"
		"uniform sampler2D mytexture;\n"
		"uniform bool combinedMode;\n"
		"in vec2 v2UVcoords;\n"
		"out vec4 outputColor;\n"
		"void main()\n"
		"{\n"
		"   outputColor = texture(mytexture, v2UVcoords);\n"
		"	if (combinedMode) {\n"
		"		outputColor *= vec4(1.0f, 0.5f, 0.5f, 1.0f);\n"
		"	}\n"
		"}\n"
		);

Single Pass Instanced 

之前,我们提到了使用渲染目标数组的可能性。渲染目标数组是立体渲染的自然解决方案。眼睛纹理共享格式和大小,并限定它们在渲染目标数组中使用。但是使用几何着色器导出数组切片是一个很大的缺点。我们真正想要的是能够从顶点着色器导出渲染目标数组索引,以更容易集成和更好的性能。

从顶点着色器导出渲染目标数组索引的能力实际上存在于某些GPU和API中,并且变得越来越普遍。在DX11上,此功能作为功能选项VPAndRTArrayIndexFromAnyShaderFeingRasterizer提供。

现在我们可以确定我们将在渲染目标数组的哪个切片上渲染,我们如何选择切片?我们使用现有的单通双宽基础架构。我们可以使用unity_StereoEyeIndex填充着色器中的SV_RenderTargetArrayIndex语义。在API端,我们不再需要切换视口,因为同一视口可以用于渲染目标数组的两个切片。我们已经将矩阵配置为可从顶点着色器索引。

虽然我们仍然可以使用现有的技术,输出两个绘制并在每次绘制之前切换恒定缓冲区中的unity_StereoEyeIndex值,但有一种有效的技术。我们可以使用GPU实例发送单个绘制调用,并允许GPU通过两只眼睛倍增我们的绘制。我们可以将现有的draw实例数量翻一番。然后,我们可以解码顶点着色器中的实例ID,以确定我们要渲染到哪个眼睛。

使用此技术的最大影响是,我们将在API端生成的绘制调用数量减少了一半,节省了部分CPU时间。此外,GPU本身能够更有效地处理绘制,即使生成了相同的工作量,因为它不需要处理两个单个绘制调用。我们还通过不必在绘制之间更改视口来最大限度地减少状态更新,就像我们使用传统的单通道技术所做的那样。

GPU Instance的具体使用方法可以参考LearnOpenGL - Instancing

 我们看看咋使用GPU Instance实现Single Pass Instanced

 

 

 Multi View 

虚拟现实应用程序需要从不同的视角渲染所有场景两次,以创造深度的错觉。通过简单地使用不同的视图和透视矩阵渲染所有内容两次来执行此操作并不是最佳的,因为它需要多次设置一个基本相同的绘制调用。GL_OVR_multiview 扩展通过允许一个绘制调用渲染到数组纹理的多个纹理层来解决这个问题,从而消除了设置多个绘制调用的开销。然后,为附着的数组纹理中的每个纹理层调用一次顶点和片段着色器,并可以访问变量g_ViewID_OVR,该变量可用于为每个图层选择视图相关输入。使用此机制,可以使用视图和投影矩阵数组而不是单个矩阵,着色器可以使用g_ViewID_OVR为每个图层选择正确的矩阵,允许一个绘制调用从多个眼睛位置渲染,而开销很少。

Multi-View是某些OpenGL/OpenGL ES实现的扩展,其中驱动程序本身在两只眼睛上多路复用单个绘制调用。驱动程序不显式实例化绘制调用并将实例解码为着色器中的眼睛索引,而是负责复制绘制并在着色器中创建数组索引(通过g_ViewID)。

有一个与立体实例不同的底层实现细节:驱动程序本身确定渲染目标,而不是显式选择要光栅化的渲染目标数组层的顶点着色器。g_ViewID用于计算视图相关状态,而不是选择渲染目标。在使用中,它对开发人员来说并不起什么作用,但它是一个有趣的细节。

我们看看Multi View在OpenGL ES的实现方法:

1、设置多视图帧缓冲区对象
在使用扩展之前,应检查它是否可用,这可以使用以下代码完成,该代码查找GL_OVR_multiview 扩展。如果需要,类似的代码可用于检查GL_OVR_multiview2 或GL_OVR_multiview_multisampled_render_to_texture 扩展。

const GLubyte* extensions = GL_CHECK(glGetString(GL_EXTENSIONS));
char * found_extension = strstr ((const char*)extensions, "GL_OVR_multiview");
if (NULL == found_extension)
{
    LOGI("OpenGL ES 3.0 implementation does not support GL_OVR_multiview extension.\n");
    exit(EXIT_FAILURE);
}

但是,即使支持扩展,也可能在GL标头中无法使用gFramebufferTextureMultiviewOVR函数。如果是这种情况,可以使用eglGetProc访问函数,如以下代码所示。

typedef void (*PFNGLFRAMEBUFFERTEXTUREMULTIVIEWOVR)(GLenum, GLenum, GLuint, GLint, GLint, GLsizei);
PFNGLFRAMEBUFFERTEXTUREMULTIVIEWOVR glFramebufferTextureMultiviewOVR;
glFramebufferTextureMultiviewOVR =
                (PFNGLFRAMEBUFFERTEXTUREMULTIVIEWOVR)eglGetProcAddress ("glFramebufferTextureMultiviewOVR");
if (!glFramebufferTextureMultiviewOVR)
{
    LOGI("Can not get proc address for glFramebufferTextureMultiviewOVR.\n");
    exit(EXIT_FAILURE);
}

如果此调用成功,则可以将gFramebufferTextureMultiviewOVR用作普通的gl函数。

下面的代码设置了一个帧缓冲区对象,以便渲染到多视图。颜色附着和深度附着都是具有2层的数组纹理,并且在此帧缓冲区对象上使用的每个绘制调用都将渲染到这两个层。重要的是,所有附件的层数必须相同,否则帧缓冲区将不完整。帧缓冲区对象必须绑定到GL_Draw_FRAMEBFER,否则gFramebufferTextureMultiviewOVR调用将给出一个无效操作错误。为帧缓冲区对象设置的视图数与绘制时当前着色器程序中声明的视图数匹配也很重要,否则绘制调用将给出无效操作错误。下一节将展示如何创建可用于多视图渲染的着色器程序。

bool setupFBO(int width, int height)
{
    // Create array texture
    GL_CHECK(glGenTextures(1, &frameBufferTextureId));
    GL_CHECK(glBindTexture(GL_TEXTURE_2D_ARRAY, frameBufferTextureId));
    GL_CHECK(glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
    GL_CHECK(glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
    GL_CHECK(glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, width, height, 2));
    /* Initialize FBO. */
    GL_CHECK(glGenFramebuffers(1, &frameBufferObjectId));
    /* Bind our framebuffer for rendering. */
    GL_CHECK(glBindFramebuffer(GL_DRAW_FRAMEBUFFER, frameBufferObjectId));
    /* Attach texture to the framebuffer. */
    GL_CHECK(glFramebufferTextureMultiviewOVR(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                                              frameBufferTextureId, 0, 0, 2));
    /* Create array depth texture */
    GL_CHECK(glGenTextures(1, &frameBufferDepthTextureId));
    GL_CHECK(glBindTexture(GL_TEXTURE_2D_ARRAY, frameBufferDepthTextureId));
    GL_CHECK(glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_DEPTH_COMPONENT24, width, height, 2));
    /* Attach depth texture to the framebuffer. */
    GL_CHECK(glFramebufferTextureMultiviewOVR(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
                                              frameBufferDepthTextureId, 0, 0, 2));
    /* Check FBO is OK. */
    GLenum result = GL_CHECK(glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER));
    if (result != GL_FRAMEBUFFER_COMPLETE)
    {
        LOGE("Framebuffer incomplete at %s:%i\n", __FILE__, __LINE__);
        /* Unbind framebuffer. */
        GL_CHECK(glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0));
        return false;
    }
    return true;
}

2、在着色器中使用多视图渲染到多个图层
以下代码显示了用于多视图渲染的着色器。只有顶点着色器包含多视图特定的代码。它启用GL_OVR_multiview扩展,并将布局中的num_views变量设置为2。此数量需要与使用gFramebufferTextureMultiviewOVR附加到帧缓冲区的视图数量相同。着色器采用视图投影矩阵数组(视图和投影矩阵相乘),而不是仅采用一个,并通过使用g_ViewID_OVR索引来选择要使用的矩阵,该索引给出了要渲染到的当前纹理层的索引。这允许我们为不同的图层拥有不同的相机位置和投影,使我们可以在VR应用程序中从两个眼睛位置进行渲染,只需一次绘制调用。在这种情况下,只有一个模型矩阵,因为模型不会为不同的层移动。在这种情况下,只有g_Position受g_ViewID_OVR值的影响,这意味着此着色器只需要GL_OVR_multiview扩展,而不需要GL_OVR_multiview2扩展。为了也根据g_ViewID_OVR(或其他顶点输出)更改法线,需要GL_OVR_multiview2。 

#version 300 es
#extension GL_OVR_multiview : enable
layout(num_views = 2) in;
in vec3 vertexPosition;
in vec3 vertexNormal;
uniform mat4 modelViewProjection[2];
uniform mat4 model;
out vec3 v_normal;
void main()
{
    gl_Position = modelViewProjection[gl_ViewID_OVR] * vec4(vertexPosition, 1.0);
    v_normal = (model * vec4(vertexNormal, 0.0f)).xyz;
}
#version 300 es
precision mediump float;
in vec3 v_normal;
out vec4 f_color;
vec3 light(vec3 n, vec3 l, vec3 c)
{
    float ndotl = max(dot(n, l), 0.0);
    return ndotl * c;
}
void main()
{
    vec3 albedo = vec3(0.95, 0.84, 0.62);
    vec3 n = normalize(v_normal);
    f_color.rgb = vec3(0.0);
    f_color.rgb += light(n, normalize(vec3(1.0)), vec3(1.0));
    f_color.rgb += light(n, normalize(vec3(-1.0, -1.0, 0.0)), vec3(0.2, 0.23, 0.35));
    f_color.a = 1.0;
}

该程序可以以与任何其他程序相同的方式设置。viewProjection矩阵必须设置为矩阵数组均匀,如下代码所示。在这个例子中,投影矩阵是相同的,但对于VR,通常会为每只眼睛使用不同的投影矩阵。本教程后面的示例将渲染到2个以上的层,并将在每个层使用不同的透视矩阵,这就是这里有多个透视矩阵的原因。在这种情况下,相机位置在x方向上设置为-1.5和1.5,两者都位于场景的中心。 

/* M_PI_2 rad = 90 degrees. */
projectionMatrix[0] = Matrix::matrixPerspective(M_PI_2, (float)width / (float)height, 0.1f, 100.0f);
projectionMatrix[1] = Matrix::matrixPerspective(M_PI_2, (float)width / (float)height, 0.1f, 100.0f);
/* Setting up model view matrices for each of the */
Vec3f leftCameraPos =  {-1.5f, 0.0f, 4.0f};
Vec3f rightCameraPos = {1.5f, 0.0f, 4.0f};
Vec3f lookAt =         {0.0f, 0.0f, -4.0f};
Vec3f upVec =          {0.0f, 1.0f, 0.0f};
viewMatrix[0] = Matrix::matrixCameraLookAt(leftCameraPos,  lookAt, upVec);
viewMatrix[1] = Matrix::matrixCameraLookAt(rightCameraPos, lookAt, upVec);
modelViewProjectionMatrix[0] = projectionMatrix[0] * viewMatrix[0] * modelMatrix;
modelViewProjectionMatrix[1] = projectionMatrix[1] * viewMatrix[1] * modelMatrix;
multiviewModelViewProjectionLocation = GL_CHECK(glGetUniformLocation(multiviewProgram, "modelViewProjection"));
multiviewModelLocation               = GL_CHECK(glGetUniformLocation(multiviewProgram, "model"));
/* Upload matrices. */
GL_CHECK(glUniformMatrix4fv(multiviewModelViewProjectionLocation, 2, GL_FALSE, modelViewProjectionMatrix[0].getAsArray()));
GL_CHECK(glUniformMatrix4fv(multiviewModelLocation, 1, GL_FALSE, modelMatrix.getAsArray()));

在绑定多视图帧缓冲区对象时,使用此程序渲染的任何内容都将从不同的视角渲染到两个纹理层,而不必执行多次绘制调用。在将VR场景渲染为每只眼睛的单独层之后,现在需要将结果渲染到屏幕上。通过绑定纹理并将其作为2D数组纹理渲染,可以轻松完成此操作。对于VR应用程序,可以设置两个视口,对于每个视口,相关纹理图层将渲染到屏幕上。这可以是将纹理简单地斑点到屏幕上,也可以在显示纹理之前对纹理执行过滤或其他后处理操作。由于纹理是数组,纹理采样操作需要vec3纹理坐标,其中最后一个坐标索引到数组中。为了使每个绘制调用选择不同的图层,可以提供具有图层索引的统一,如以下片段着色器中所示。

#version 300 es
precision mediump float;
precision mediump int;
precision mediump sampler2DArray;
in vec2 vTexCoord;
out vec4 fragColor;
uniform sampler2DArray tex;
uniform int layerIndex;
void main()
{
    fragColor = texture(tex, vec3(vTexCoord, layerIndex));
}

性能分析

 Single Pass和Single Pass Instance代表了与Multi Pass相比的有着显著CPU优势。这是因为切换到Single Pass已经节省了大部分CPU开销。
Single Pass Instance减少了绘制调用的数量,但与场景图的处理相比,这些成本相当低。
考虑到大多数现代图形驱动程序都是多线程的,绘制调用的输出可以在调度CPU线程上相当快地完成。

参考文献:

https://github.com/ikapoura/single-pass-stereo-with-openvr

Single Pass Stereo: is it worth it? – Learning by creating

VRWorks - Single Pass Stereo | NVIDIA Developer

LearnOpenGL - Instancing

Calculating Stereo Pairs

VRWorks - Multi-View Rendering (MVR) | NVIDIA Developer

https://gitea.yiem.net/QianMo/Real-Time-Rendering-4th-Bibliography-Collection/raw/branch/main/Chapter%201-24/[1891]%20[SDVR%202015]%20High%20Performance%20Stereo%20Rendering%20for%20VR.pdf

 Turing Multi-View Rendering in VRWorks | NVIDIA Technical Blog

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/105565.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

揭秘百度智能测试在测试定位领域实践

作者 | intelligents 前几篇,分别介绍了测试活动测试输入、测试执行、测试分析、测试定位和测试评估五个步骤中测试输入、执行、分析、评估的智能化研究和实践,本章节重点介绍测试定位环节的智能化实践。 测试定位的主要作用是在构建失败或问题发生后&…

傻白探索Chiplet,国内外研究现状(六)

目录 一、概述 二、国外Chiplet历史与现状 2.1 AMD 2.1.1 EPYC(Naples) 2.1.2 EPYC(Rome) 2.1.3 EPYC(Milan-X ) 2.1.4 Ryzen(Matisse) 2.2 苹果 2.3 Intel 2.3.1 Alter…

【大数据技术】Spark+Flume+Kafka实现商品实时交易数据统计分析实战(附源码)

需要源码请点赞关注收藏后评论区留言私信~~~ Flume、Kafka区别和侧重点 1)Kafka 是一个非常通用的系统,你可以有许多生产者和消费者共享多个主题Topics。相比之下,Flume是一个专用工具被设计为旨在往HDFS,HBase等发送数据。它对H…

2022年我国江蓠行业现状:养殖面积、产量不断增长 进口量仍大于出口

根据观研报告网发布的《中国江蓠市场现状深度研究与发展前景预测报告(2022-2029年)》显示,江蓠属于“海藻”产业,为暖水性藻类,我国俗称 “龙须菜”、 “海菜”、 “蚝菜”。藻体紫褐色或紫黄色、绿色。 江蓠在热带、 …

Opencv(C++)笔记--霍夫变换检测直线、霍夫变换检测圆

目录 1--原理 2--Opencv API 3--实例代码 4--霍夫变换检测圆 1--原理 具体原理可参考 博客1 和 视频讲解1; 霍夫变换检测直线的核心思想是:在笛卡尔坐标系下,一条直线(两个点(x1, y1)和(x2,…

行业权威来揭秘,商用PC为什么首选12代酷睿

第12代酷睿处理器可以提供更卓越的性能,凭借架构先进性让商用台式机和笔记本电脑为用户带来更好的体验,帮助企业和员工效率倍增。 作者|九月 来源| PConline 想要让办公效率进一步提升,一台强大的PC设备是必不可少的生产力和内容创作工…

有什么适合零基础的人做的副业兼职

互联网上有很多套路。这是不可预防的。只要你敢贪婪,你就会陷入别人设计的陷阱。在业余时间做兼职应该是很多人的梦想,因为他们可以在有限的时间内赚更多的钱。很多人不知道的是,其实我们赚钱的渠道很多:比如网上发文章.短视频直播.我们媒体、…

基于SpringBoot+Mybatis框架的私人影院预约系统(附源码,包含数据库文件)

基于SpringBootMybatis框架的私人影院预约系统,附源码,包含数据库文件。 非常完整的一个项目,希望能对大家有帮助哈。 本系统的完整源码以及数据库文件都在文章结尾处,大家自行获取即可。 项目简介 该项目设计了基于SpringBoo…

Spring MVC—Spring MVC概述

文章目录Java web的发展历史一.Model I和Model II1.Model I开发模式2.Model II开发模式二. MVC模式SpringMVC 的工作原理和流程springmvc 的拦截器Spring和SpringMVC的区别————————————————————————————————Java web的发展历史 一.Model I和M…

VS Code debug调试时无法查看变量内容【已解决】

问题场景:新换成的vscode编译软件,但是在debug调试时发现与QtCreator不同,无法直接查看变量,显示的都是地址或其他。 比如:QString或QStringList无法查看具体的内容,正常是这样显示的,反正我不…

Linux神器——vim

目录 一、vim基本概念 二、vim基本操作 三、vim正常模式命令集 四、vim末行模式命令集 五、vim操作总结 六、vim界面配置 vi/vim的区别简单点来说,它们都是多模式编辑器,不同的是vim是vi的升级版本,它不仅兼容vi的所有指令,而…

上班15年后,普通程序员能实现财富自由吗?

对于职业生涯还没有开挂的普通程序员来说,有可能实现财务自由吗? 先来说下财务自由的最低标准 北上广深:身价3000万,含房产1000万、现金2000万。 杭州、南京、成都等二线城市:身价1500万,含500万房产、现…

集成底座双K8S集群扩展升级方案

集成底座方案是应用于企业信息化建设的集成整合阶段,通过建立统一、标准、柔性、可复用、可扩展的IT架构,解决企业信息化建设过程中缺乏整体规划、集成整合难度大、安全管控不到位等问题,强化企业信息化的架构建设、集成整合、数据治理、安全…

某鱼兼职并不是那么好做,钱也不是漫天要价

文章目录一、背景二、雇主的期望2.1、jinja2代码三、题主的期望3.1、删除功能3.2、前端体现3.3、留言列表实现降序3.4、效果显示四、总结一、背景 上周某鱼推送过来的单子多到题主应接不暇,不得已拒绝了几单,但是接下来的单子呢又不那么顺利,…

提速3.7倍!何恺明团队再发新作,更快更高效的FLIP模型:通过Masking扩展语言-图像预训练(附论文原文下载)

原创/文 BFT机器人 研究论文地址:https://arxiv.org/abs/2212.00794 计算机视觉和深度学习领域大神何恺明携团队再发新作!论文围绕近来火热的CLIP(Contrastive Language-Image Pre-Training)模型展开研究,并提出了一种…

Python怎么进行时区的转换

pytz 是一个用于处理时区的 Python 库,它为 Python 提供了对时区的支持。 它提供了大量的时区信息,包括时区名称、偏移量、是否使用夏令时等。你可以使用 pytz 库来处理本地时间、UTC 时间和其他时区之间的转换。 它提供了许多函数来帮助你处理时区相关的信息。 …

C++之多态(中篇)(最全总结)

这里接上面C之多态(上篇) 本篇目录4.多态的原理4.2 多态的原理4.3 C 11 override和final4.4 重载、重写(覆盖)、隐藏(重定义)的对比 (函数之间的关系)5.抽象类5.1概念5.2接口继承和实…

三、基于kubeadm安装kubernetes1.25集群第二篇

在上一篇中我们已经安装kubernetes要求做了服务器初始化,看这篇之前,建议先看下上篇:https://blog.csdn.net/u011837804/article/details/128350651 那我们正式开始kubernetes1.26集群安装 1、每台机器安装docker20.10.22 docker的安装细节…

数据结构训练营4

开启蓝桥杯备战计划,每日练习算法一题!!坚持下去,想必下一年的蓝桥杯将会有你!!笔者是在力扣上面进行的刷题!!由于是第一次刷题!找到的题目也不咋样!所以&…

itop-imx8m开发板gstreamer日志级别设置

gst 的日志等级分为 none(0)error(1) warning(2) info(3) debug(4) log(5)。默认 gst 的日志等级为 1,即 error 打印,出错时会打印。 1)全局日志级别设置 如果需要更高级别打印,修改环境变量 GST_DEBUG 即可。如需要 warning 级别…