Android OpenGL ES详解——实例化

目录

一、实例化

1、背景

2、概念

实例化、实例数量

gl_InstanceID

应用举例

二、实例化数组

1、概念

2、应用举例

三、应用举例——小行星带

1、不使用实例化

2、使用实例化

四、总结


一、实例化

1、背景

假如你有一个有许多模型的场景,而这些模型的顶点数据都一样,只是进行了不同的世界空间的变换。想象一下,有一个场景中充满了草叶:每根草都是几个三角形组成的。你可能需要绘制很多的草叶,最终一次渲染循环中就肯能有成千上万个草需要绘制了。因为每个草叶只是由几个三角形组成,绘制一个几乎是即刻完成,但是数量巨大以后,执行起来就很慢了。

如果我们渲染这样多的物体的时候,也许代码会写成这样:

for(GLuint i = 0; i < amount_of_models_to_draw; i++)
{
    DoSomePreparations(); //在这里绑定VAO、绑定纹理、设置uniform变量等
    glDrawArrays(GL_TRIANGLES, 0, amount_of_vertices);
}

像这样绘制出你模型的其他实例,多次绘制之后,很快将达到一个瓶颈,这是因为你glDrawArraysglDrawElements这样的函数(Draw call)过多。这样渲染顶点数据,会明显降低执行效率,这是因为OpenGL在它可以绘制你的顶点数据之前必须做一些准备工作(比如告诉GPU从哪个缓冲读取数据,以及在哪里找到顶点属性,所有这些都会使CPU到GPU的总线变慢)。所以即使渲染顶点超快,而多次给你的GPU下达这样的渲染命令却未必。

如果我们能够将数据一次发送给GPU,就会更方便,然后告诉OpenGL使用一个绘制函数,将这些数据绘制为多个物体。这就是我们将要展开讨论的实例化(Instancing)

2、概念

实例化、实例数量

实例化是一种只调用一次渲染函数却能绘制出很多物体的技术,它节省渲染物体时从CPU到GPU的通信时间,而且只需做一次即可。要使用实例化渲染,我们必须将glDrawArraysglDrawElements各自改为glDrawArraysInstancedglDrawElementsInstanced。这些用于实例化的函数版本需要设置一个额外的参数,叫做实例数量(Instance Count),它设置我们打算渲染实例的数量。这样我们就只需要把所有需要的数据发送给GPU一次就行了,然后告诉GPU它该如何使用一个函数来绘制所有这些实例。

gl_InstanceID

就其本身而言,这个函数用处不大。渲染同一个物体一千次对我们来说没用,因为每个渲染出的物体不仅相同而且还在同一个位置;我们只能看到一个物体!出于这个原因GLSL在着色器中嵌入了另一个内建变量,叫做gl_InstanceID

在通过实例化绘制时,gl_InstanceID的初值是0,它在每个实例渲染时都会增加1。如果我们渲染43个实例,那么在顶点着色器gl_InstanceID的值最后就是42。每个实例都拥有唯一的值意味着我们可以索引到一个位置数组,并将每个实例摆放在世界空间的不同的位置上。

应用举例

我们调用一个实例化渲染函数,在标准化设备坐标中绘制一百个2D四边形来看看实例化绘制的效果是怎样的。通过对一个储存着100个偏移量向量的索引,我们为每个实例四边形添加一个偏移量。最后,窗口被排列精美的四边形网格填满:

每个四边形是2个三角形所组成的,因此总共有6个顶点。每个顶点包含一个2D标准设备坐标位置向量和一个颜色向量。下面是例子中所使用的顶点数据,每个三角形为了适应屏幕都很小:

GLfloat quadVertices[] = {
    //  ---位置---   ------颜色-------
    -0.05f,  0.05f,  1.0f, 0.0f, 0.0f,
     0.05f, -0.05f,  0.0f, 1.0f, 0.0f,
    -0.05f, -0.05f,  0.0f, 0.0f, 1.0f,

    -0.05f,  0.05f,  1.0f, 0.0f, 0.0f,
     0.05f, -0.05f,  0.0f, 1.0f, 0.0f,
     0.05f,  0.05f,  0.0f, 1.0f, 1.0f
};

片段着色器接收从顶点着色器发送来的颜色向量,设置为它的颜色输出,从而为四边形上色:

#version 330 core
in vec3 fColor;
out vec4 color;

void main()
{
    color = vec4(fColor, 1.0f);
}

到目前为止没有什么新内容,但顶点着色器开始变得有意思了:

到目前为止没有什么新内容,但顶点着色器开始变得有意思了:

#version 330 core
layout (location = 0) in vec2 position;
layout (location = 1) in vec3 color;

out vec3 fColor;

uniform vec2 offsets[100];

void main()
{
    vec2 offset = offsets[gl_InstanceID];
    gl_Position = vec4(position + offset, 0.0f, 1.0f);
    fColor = color;
}

这里我们定义了一个uniform数组,叫offsets,它包含100个偏移量向量。在顶点着色器里,我们接收一个对应着当前实例的偏移量,这是通过使用 gl_InstanceID来索引offsets得到的。如果我们使用实例化绘制100个四边形,使用这个顶点着色器,我们就能得到100位于不同位置的四边形。

我们一定要设置偏移位置,在游戏循环之前我们用一个嵌套for循环计算出它来:

glm::vec2 translations[100];
int index = 0;
GLfloat offset = 0.1f;
for(GLint y = -10; y < 10; y += 2)
{
    for(GLint x = -10; x < 10; x += 2)
    {
        glm::vec2 translation;
        translation.x = (GLfloat)x / 10.0f + offset;
        translation.y = (GLfloat)y / 10.0f + offset;
        translations[index++] = translation;
    }
}

这里我们创建100个平移向量,它包含着10×10格子所有位置。除了生成translations数组外,我们还需要把这些位移数据发送到顶点着色器的uniform数组:

hader.Use();
for(GLuint i = 0; i < 100; i++)
{
    stringstream ss;
    string index;
    ss << i;
    index = ss.str();
    GLint location = glGetUniformLocation(shader.Program, ("offsets[" + index + "]").c_str())
    glUniform2f(location, translations[i].x, translations[i].y);
}

这一小段代码中,我们将for循环计数器i变为string,接着就能动态创建一个为请求的uniform的location创建一个location字符串。将offsets数组中的每个条目,我们都设置为相应的平移向量。

现在所有的准备工作都结束了,我们可以开始渲染四边形了。用实例化渲染来绘制四边形,我们需要调用glDrawArraysInstancedglDrawElementsInstanced,由于我们使用的不是索引绘制缓冲,所以我们用的是glDrawArrays对应的那个版本glDrawArraysInstanced

glBindVertexArray(quadVAO);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);  
glBindVertexArray(0);

glDrawArraysInstanced的参数和glDrawArrays一样,除了最后一个参数设置了我们打算绘制实例的数量。我们想展示100个四边形,它们以10×10网格形式展现,所以这儿就是100.运行代码,你会得到100个相似的有色四边形。

二、实例化数组

1、概念

在这种特定条件下,前面的实现很好,但是当我们有100个实例的时候(这很正常),最终我们将碰到uniform数据数量的上线。为避免这个问题另一个可替代方案是实例化数组(Instanced Array),它使用顶点属性来定义,这样就允许我们使用更多的数据了,当顶点着色器渲染一个新实例时它才会被更新。

使用顶点属性,每次运行顶点着色器都将让GLSL获取到下个顶点属性集合,它们属于当前顶点。当把顶点属性定义为实例数组时,顶点着色器只更新每个实例的顶点属性的内容而不是顶点的内容。这使我们在每个顶点数据上使用标准顶点属性,用实例数组来储存唯一的实例数据。

2、应用举例

为了展示一个实例化数组的例子,我们将采用前面的例子,把偏移uniform表示为一个实例数组。我们不得不增加另一个顶点属性,来更新顶点着色器。

#version 330 core
layout (location = 0) in vec2 position;
layout (location = 1) in vec3 color;
layout (location = 2) in vec2 offset;

out vec3 fColor;

void main()
{
    gl_Position = vec4(position + offset, 0.0f, 1.0f);
    fColor = color;
}

我们不再使用gl_InstanceID,可以直接用offset属性,不用先在一个大uniform数组里进行索引。

因为一个实例化数组实际上就是一个和位置和颜色一样的顶点属性,我们还需要把它的内容储存为一个顶点缓冲对象里,并把它配置为一个属性指针。我们首先将平移变换数组贮存到一个新的缓冲对象上:

GLuint instanceVBO;
glGenBuffers(1, &instanceVBO);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec2) * 100, &translations[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);

之后我们还需要设置它的顶点属性指针,并开启顶点属性:

glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (GLvoid*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glVertexAttribDivisor(2, 1);

代码中有意思的地方是,最后一行,我们调用了 glVertexAttribDivisor。这个函数告诉OpenGL什么时候去更新顶点属性的内容到下个元素。它的第一个参数是提到的顶点属性,第二个参数是属性除数(attribute divisor)。默认属性除数是0,告诉OpenGL在顶点着色器的每次迭代更新顶点属性的内容。把这个属性设置为1,我们就是告诉OpenGL我们打算在开始渲染一个新的实例的时候更新顶点属性的内容。设置为2代表我们每2个实例更新内容,依此类推。把属性除数设置为1,我们可以高效地告诉OpenGL,location是2的顶点属性是一个实例数组(instanced array)。

如果我们现在再次使用glDrawArraysInstanced渲染四边形,我们会得到下面的输出:

和前面的一样,但这次是使用实例数组实现的,它使我们为绘制实例向顶点着色器传递更多的数据(内存允许我们存多少就能存多少)。

你还可以使用gl_InstanceID从右上向左下缩小每个四边形。

void main()
{
    vec2 pos = position * (gl_InstanceID / 100.0f);
    gl_Position = vec4(pos + offset, 0.0f, 1.0f);
    fColor = color;
}

结果是第一个实例的四边形被绘制的非常小,随着绘制实例的增加,gl_InstanceID越来越接近100,这样更多的四边形会更接近它们原来的大小。这是一种很好的将gl_InstanceID与实例数组结合使用的法则:

这些例子不是实例的好例子,不过挺有意思的。它们可以让你对实例的工作方式有一个概括的理解,但是当绘制拥有极大数量的相同物体的时候,它极其有用,现在我们还没有展示呢。出于这个原因,我们将在接下来的部分进入太空来看看实例渲染的威力。

三、应用举例——小行星带

想象一下,在一个场景中有一个很大的行星,行星周围有一圈小行星带。这样一个小行星大可能包含成千上万的石块,对于大多数显卡来说几乎是难以完成的渲染任务。这个场景对于实例渲染来说却不再话下,由于所有小行星可以使用一个模型来表示。每个小行星使用一个变换矩阵就是一个经过少量变化的独一无二的小行星了。

1、不使用实例化

为了展示实例渲染的影响,我们先不使用实例渲染,来渲染一个小行星围绕行星飞行的场景。

为了得到我们理想中的效果,我们将为每个小行星生成一个变换矩阵,作为它们的模型矩阵。变换矩阵先将小行星平移到行星带上,我们还要添加一个随机位移值来作为偏移量,这样才能使行星带更自然。接着我们应用一个随机缩放,以及一个随机旋转向量。最后,变换矩阵就会将小行星变换到行星的周围,同时使它们更自然,每个行星都有别于其他的。

GLuint amount = 1000;
glm::mat4* modelMatrices;
modelMatrices = new glm::mat4[amount];
srand(glfwGetTime()); // initialize random seed
GLfloat radius = 50.0;
GLfloat offset = 2.5f;
for(GLuint i = 0; i < amount; i++)
{
    glm::mat4 model;
    // 1. Translation: displace along circle with 'radius' in range [-offset, offset]
    GLfloat angle = (GLfloat)i / (GLfloat)amount * 360.0f;
    GLfloat displacement = (rand() % (GLint)(2 * offset * 100)) / 100.0f - offset;
    GLfloat x = sin(angle) * radius + displacement;
    displacement = (rand() % (GLint)(2 * offset * 100)) / 100.0f - offset;
    GLfloat y = displacement * 0.4f; // y value has smaller displacement
    displacement = (rand() % (GLint)(2 * offset * 100)) / 100.0f - offset;
    GLfloat z = cos(angle) * radius + displacement;
    model = glm::translate(model, glm::vec3(x, y, z));
    // 2. Scale: Scale between 0.05 and 0.25f
    GLfloat scale = (rand() % 20) / 100.0f + 0.05;
    model = glm::scale(model, glm::vec3(scale));
    // 3. Rotation: add random rotation around a (semi)randomly picked rotation axis vector
    GLfloat rotAngle = (rand() % 360);
    model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));
    // 4. Now add to list of matrices
    modelMatrices[i] = model;
}

这段代码看起来还是有点吓人,但我们基本上是沿着一个半径为radius的圆圈变换小行星的x和y的值,让每个小行星在-offset和offset之间随机生成一个位置。我们让y变化的更小,这让这个环带就会成为扁平的。接着我们缩放和旋转变换,把结果储存到一个modelMatrices矩阵里,它的大小是amount。这里我们生成1000个模型矩阵,每个小行星一个。

加载完天体和小行星模型后,编译着色器,渲染代码是这样的:

// 绘制行星
shader.Use();
glm::mat4 model;
model = glm::translate(model, glm::vec3(0.0f, -5.0f, 0.0f));
model = glm::scale(model, glm::vec3(4.0f, 4.0f, 4.0f));
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
planet.Draw(shader);

// 绘制石头
for(GLuint i = 0; i < amount; i++)
{
    glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(modelMatrices[i]));
    rock.Draw(shader);
}

我们先绘制天体模型,要把它平移和缩放一点以适应场景,接着,我们绘制amount数量的小行星,它们按照我们所计算的结果进行变换。在我们绘制每个小行星之前,我们还得先在着色器中设置相应的模型变换矩阵。

结果是一个太空样子的场景,我们可以看到有一个自然的小行星带:

这个场景包含1001次渲染函数调用,每帧渲染1000个小行星模型。

当我们开始增加数量的时候,很快就会注意到帧数的下降,而且下降的厉害。当我们设置为2000的时候,场景慢得已经难以移动了。

2、使用实例化

我们再次使用实例渲染来渲染同样的场景。我们先得对顶点着色器进行一点修改:

#version 330 core
layout (location = 0) in vec3 position;
layout (location = 2) in vec2 texCoords;
//实例数组
layout (location = 3) in mat4 instanceMatrix;

out vec2 TexCoords;

uniform mat4 projection;
uniform mat4 view;

void main()
{
    gl_Position = projection * view * instanceMatrix * vec4(position, 1.0f);
    TexCoords = texCoords;
}

我们不再使用模型uniform变量,取而代之的是把一个mat4的顶点属性,送一我们可以将变换矩阵储存为一个实例数组(instanced array)。然而,当我们声明一个数据类型为顶点属性的时候,它比一个vec4更大,是有些不同的。顶点属性被允许的最大数据量和vec4相等。因为一个mat4大致和4个vec4相等,我们为特定的矩阵必须保留4个顶点属性。因为我们将它的位置赋值为3个列的矩阵,顶点属性的位置就会是3、4、5和6。

然后我们必须为这4个顶点属性设置属性指针,并将其配置为实例数组:

for(GLuint i = 0; i < rock.meshes.size(); i++)
{
    GLuint VAO = rock.meshes[i].VAO;
    // Vertex Buffer Object
    GLuint buffer;
    glBindVertexArray(VAO);
    glGenBuffers(1, &buffer);
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    glBufferData(GL_ARRAY_BUFFER, amount * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);
    // Vertex Attributes
    GLsizei vec4Size = sizeof(glm::vec4);
    glEnableVertexAttribArray(3);
    glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (GLvoid*)0);
    glEnableVertexAttribArray(4);
    glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (GLvoid*)(vec4Size));
    glEnableVertexAttribArray(5);
    glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (GLvoid*)(2 * vec4Size));
    glEnableVertexAttribArray(6);
    glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (GLvoid*)(3 * vec4Size));

    glVertexAttribDivisor(3, 1);
    glVertexAttribDivisor(4, 1);
    glVertexAttribDivisor(5, 1);
    glVertexAttribDivisor(6, 1);

    glBindVertexArray(0);
}

要注意的是我们将Mesh的VAO变量声明为一个public(公有)变量,而不是一个private(私有)变量,所以我们可以获取它的顶点数组对象。这不是最干净的方案,但这能较好的适应本教程。若没有这点hack,代码就干净了。我们声明了OpenGL该如何为每个矩阵的顶点属性的缓冲进行解释,每个顶点属性都是一个实例数组。

下一步我们再次获得网格的VAO,这次使用glDrawElementsInstanced进行绘制:

// Draw meteorites
instanceShader.Use();
for(GLuint i = 0; i < rock.meshes.size(); i++)
{
    glBindVertexArray(rock.meshes[i].VAO);
    glDrawElementsInstanced(
        GL_TRIANGLES, rock.meshes[i].vertices.size(), GL_UNSIGNED_INT, 0, amount
    );
    glBindVertexArray(0);
}

这里我们绘制和前面的例子里一样数量(amount)的小行星,只不过是使用的实例渲染。结果是相似的,但你会看在开始增加数量以后效果的不同。不实例渲染,我们可以流畅渲染1000到1500个小行星。而使用了实例渲染,我们可以设置为100000,每个模型由576个顶点,这几乎有5千7百万个顶点,而且帧率没有丝毫下降!

上图渲染了十万小行星,半径为150.0f,偏移等于25.0f。

有些机器渲染十万可能会有点吃力,所以尝试修改这个数量知道你能获得可以接受的帧率。

四、总结

就像你所看到的,在合适的条件下,实例渲染对于你的显卡来说和普通渲染有很大不同。处于这个理由,实例渲染通常用来渲染草、草丛、粒子以及像这样的场景,基本上来讲只要场景中有很多重复物体,使用实例渲染都会获得好处。

参考文章

https://learnopengl-cn.readthedocs.io/zh/latest/04%20Advanced%20OpenGL/10%20Instancing/

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2024数维杯问题C:脉冲星定时噪声推断和大气时间信号的时间延迟推断的建模完整思路 模型 代码结果

&#xff08;Modeling of pulsar timing noise deduction and atmospheric time delay deduction of time signals&#xff09; 脉冲星是一种连续而稳定的快速旋转的中子星&#xff0c;为它们赢得了“宇宙的李温室”的绰号。脉冲星的空间观测对深空航天器的导航和时间标准的维…

Shell基础2

声明&#xff01; 学习视频来自B站up主 **泷羽sec** 有兴趣的师傅可以关注一下&#xff0c;如涉及侵权马上删除文章&#xff0c;笔记只是方便各位师傅的学习和探讨&#xff0c;文章所提到的网站以及内容&#xff0c;只做学习交流&#xff0c;其他均与本人以及泷羽sec团…

基于微信小程序的校园超市购物系统设计与实现,LW+源码+讲解

摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本超市购物系统就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短时间内处理完毕庞大的数据信息&a…

【golang-技巧】-线上死锁问题排查-by pprof

1.背景 由于目前项目使用 cgo golang 本地不能debug, 发生死锁问题&#xff0c;程序运行和期待不一致&#xff0c;通过日志排查可以大概率找到 阻塞范围&#xff0c;但是不能找到具体问题在哪里&#xff0c;同时服务器 通过k8s daemonset 部署没有更好的方式暴露端口 获取ppr…

【Visual Studio】设置文件目录

打开属性 输出目录&#xff1a;$(SolutionDir)bin\$(Platform)\$(Cinfiguration)\ 中间目录&#xff1a;$(SolutionDir)bin\intermediates\$(Platform)\$(Cinfiguration)\

智谱AI清影升级:引领AI视频进入音效新时代

前几天智谱推出了新清影,该版本支持4k、60帧超高清画质、任意尺寸&#xff0c;并且自带音效的10秒视频,让ai生视频告别了"哑巴时代"。 智谱AI视频腾空出世&#xff0c;可灵遭遇强劲挑战&#xff01;究竟谁是行业翘楚&#xff1f;(附测评案例)之前智谱出世那时体验了一…

Datawhale模型压缩技术Task2之模型剪枝

模型剪枝 模型剪枝介绍何为剪枝&#xff08;What is Pruning?&#xff09;剪枝类型非结构化剪枝结构化剪枝半结构化剪枝 剪枝范围局部剪枝全局剪枝 剪枝粒度细粒度剪枝基于模式的剪枝向量级剪枝内核级剪枝通道级剪枝 为何剪枝&#xff08;Why Pruning?&#xff09;剪枝标准&a…

雨晨 Fix 24H2 Windows 11 iot 企业版 ltsc 2024 极简 2合1 26100.2448

映像的详细信息: 雨晨 Fix 24H2 Windows 11 iot 企业版 ltsc 2024 极简 2合1 26100.2448 索引: 1 名称: Windows 11 IoT 企业版 LTSC 2024 极简V1 26100.2448 (传统legacy资源管理器) 描述: Windows 11 IoT 企业版 LTSC 2024 极简V1 26100.2448 By YCDISM v2025 2024-11-15 大…

【Qt聊天室】客户端实现总结

目录 1. 项目概述 2. 功能实现 2.1 主窗口设计 2.2 功能性窗口 2.3 主界面功能实现 2.4 聊天界面功能实现 2.5 个人信息功能开发 2.6 用户信息界面设置功能 2.7 单聊与群聊 2.8 登录窗口 2.9 消息功能 3. 核心设计逻辑 3.1 核心类 3.2 前后端交互与DataCenter 4…

3、.Net UI库:CSharpSkin - 开源项目研究文章

CSharpSkin(C# 皮肤)是一个基于C#语言开发的UI框架&#xff0c;它允许开发者使用C#和.NET技术栈来创建跨平台的桌面应用程序。CSharpSkin框架通常用于实现具有自定义外观和感觉的应用程序界面&#xff0c;它提供了一套丰富的控件和组件&#xff0c;以及灵活的样式和布局系统。 …

JUC包中常用类解析

目录 &#xff08;一&#xff09;Callable接口 &#xff08;1&#xff09;Callable与Runnable的区别 &#xff08;2&#xff09;Future接口 2.1Futrue接口中的方法 2.2FutureTask类 &#xff08;3&#xff09;Callable接口的使用 3.1借助FutureTask运行 3.2借助线程池运…

交友问题 | 动态规划

描述 如果有n个人&#xff0c;每个人都可以保持单身或与其他人结成一对。每个人只能找一个对象。求总共有多少种保持单身或结对的方式。用动态规划求解。 输入 输入第一行t表示测试用例的数量 对于每一个测试用例, 输入一个整数n表示人数1<n<18 输出 针对每个测试用…

【WPF】Prism库学习(一)

Prism介绍 1. Prism框架概述&#xff1a; Prism是一个用于构建松耦合、可维护和可测试的XAML应用程序的框架。它支持WPF、.NET MAUI、Uno Platform和Xamarin Forms等多个平台。对于每个平台&#xff0c;Prism都有单独的发布版本&#xff0c;并且它们在不同的时间线上独立开发。…

基于Java Springboot在线音乐试听交流网站

一、作品包含 源码数据库设计文档万字PPT全套环境和工具资源部署教程 二、项目技术 前端技术&#xff1a;Html、Css、Js、Vue 数据库&#xff1a;MySQL 后端技术&#xff1a;Java、Spring Boot、MyBatis 三、运行环境 开发工具&#xff1a;IDEA/eclipse 数据库&#xff…

# 第20章 Cortex-M4-触摸屏

第20章 Cortex-M4-触摸屏 20.1 触摸屏概述 20.1.1 常见的触摸屏分类 电阻式触摸屏、电容式触摸屏、红外式触摸屏、表面声波触摸屏 市场上用的最多的是电阻式触摸屏与电容式触摸屏。红外管式触摸屏多用于投影仪配套设备。 电阻式触摸屏构成&#xff1a;整个屏由均匀电阻构成…

《 C++ 修炼全景指南:二十 》不止是链表升级!跳表的核心原理与超强性能解析

摘要 这篇博客全面解析了跳表 (Skip List) 作为一种高效的链表数据结构的特性和应用。跳表以多层链表和随机化策略实现 O(log n) 的查找、插入和删除性能&#xff0c;简化了平衡树结构中常见的复杂旋转操作。通过剖析跳表的结构设计和核心操作&#xff0c;我们探讨了其在范围查…

【C++之STL】摸清 string 的模拟实现(上)

文章目录 1. 为什么要模拟实现&#xff1f;2. 基本框架搭建3. 构造函数3. 1 默认构造/from c_str3. 2 拷贝构造3. 2. 1 深浅拷贝 3. 3 fill3. 4 迭代器区间构造 4. 容量操作4. 1 size()和capacity()和empty()4. 2 clear()4. 3 resize()4. 4 reserve() 1. 为什么要模拟实现&…

【postman】怎么通过curl看请求报什么错

获取现成的curl方式&#xff1a; 1&#xff0c;拿别人给的curl 2&#xff0c;手机app界面通过charles抓包&#xff0c;点击接口复制curl 3&#xff0c;浏览器界面-开发者工具-选中接口复制curl 拿到curl之后打开postman&#xff0c;点击import&#xff0c;粘贴curl点击send&am…

【网页设计】CSS 高级技巧

目标 能够使用精灵图能够使用字体图标能够写出 CSS 三角能够写出常见的 CSS 用户界面样式能够说出常见的布局技巧 1. 精灵图 为什么需要精灵图&#xff1f;精灵图的使用精灵图课堂案例 1.1 为什么需要精灵图&#xff1f; 一个网页中往往会应用很多小的背景图像作为修饰&…

【JavaEE初阶 — 多线程】wait() notify()

1. 协调多个线程之间的执行先后顺序的方法介绍 由于线程之间是抢占式执行的&#xff0c;因此线程之间执行的先后顺序难以预知&#xff1b;但是实际开发中&#xff0c;有时候我们希望合理地协调多个线程之间的执行先后顺序。 拓展&#xff1a; wait() 和 sleep() 的区别 …