Metal学习笔记十三:阴影

在本章中,您将了解阴影。阴影表示表面上没有光。当另一个表面或对象使对象与光线相遮挡时,您会看到对象上的阴影。在项目中添加阴影可使您的场景看起来更逼真,并提供深度感。

阴影贴图

阴影贴图是包含场景阴影信息的纹理。当光线照射到物体上时,它会在物体后面的物体上投下阴影。
通常,您从摄像机的位置渲染场景。但是,要构建阴影贴图,您需要从光源的位置(在本例中为太阳)渲染场景。


左侧的图像显示了从摄像机位置进行的渲染,其中定向光指向下方。右侧的图像显示了从定向光位置进行的渲染。眼睛表示相机在第一张图像中的位置。

你会用到两个渲染通道:

•第一个通道:您将从光的角度渲染。由于太阳是定向的,因此您将使用正交摄像机,而不是透视相机。您只对太阳可以看到的物体的深度感兴趣,因此您不会呈现颜色纹理。在此通道中,您只会将阴影贴图渲染为深度纹理。它是灰度纹理,灰色值表示深度。黑色靠近光线,白色距离更远。
•第二个通道:您将像往常一样使用场景摄像机进行渲染,但是您将相机片段与每个阴影贴图片段进行比较。如果相机片段的深度小于该位置处的阴影贴图片段,则片段在阴影中。灯光可以在上图中看到蓝色X,因此它不在阴影中。
为什么您在这里需要两个通道?在这种情况下,您将从光源位置而不是从相机位置,渲染阴影贴图。您可以将输出保存到阴影纹理中,并将其传递到下一个渲染通道,将阴影与场景的其余部分结合在一起以制作最终图像。

入门项目

➤在Xcode中,打开本章的入门项目。

入门项目中的代码几乎与上一章相同,但没有对象ID和拾取对象代码。现在,现场有一个可见的阳光,它是唯一的灯光,在现场的中心旋转。 (请记住,太阳是一个定向矢量。场景中的太阳模型比实际的太阳更近!)
渲染太阳的代码位于DebugModel.swift的公用utility group中。您将与ForwardRenderPass中的场景分开渲染太阳,以使太阳模型不会被其余的场景所遮蔽。
有一些包含该应用程序不需要的代码的额外文件。您将在本章期间了解这些文件。

➤构建并运行应用程序。
 
没有阴影,这种渲染中的火车和树木似乎漂浮在地面上方。
添加新的阴影通道的过程类似于添加上一章的对象选择渲染通道:
1。创建新的渲染通道结构体并配置渲染通道描述符,深度模板状态和管道状态。
2。在渲染器中声明并绘制渲染通道。
3。在渲染通道中设置绘图代码。
4。从光的位置设置正交摄像头并计算必要的矩阵。
5。创建顶点着色器函数,以从光的位置绘制顶点。
尽管您在该应用程序中给太阳一个位置,但是正如您在第10章中学到的“光照基本原理”,但定向光具有方向而不是位置。因此,在这里,您将使用太阳的位置作为方向。

注意:如果您想查看调试太阳方向的方向线,就像您在前一章中所做的那样,请在 renderEncoder.endEncoding() 之前将 DebugLights.draw(lights: scene.lighting.lights, encoder: renderEncoder, uniforms: uniforms)添加到 ForwardRenderPass。

是时候添加新的阴影通道了。

1. 创建新的渲染通道

➤ 在渲染通道group中,创建一个名为 ShadowRenderPass.swift 的新 Swift 文件,并将代码替换为:

import MetalKit
struct ShadowRenderPass: RenderPass {let label: String = "Shadow Render Pass"var descriptor: MTLRenderPassDescriptor?= MTLRenderPassDescriptor()var depthStencilState: MTLDepthStencilState?= Self.buildDepthStencilState()var pipelineState: MTLRenderPipelineStatevar shadowTexture: MTLTexture?mutating func resize(view: MTKView, size: CGSize) {}func draw(commandBuffer: MTLCommandBuffer,scene: GameScene,uniforms: Uniforms,params: Params
){
} }

此代码创建符合 RenderPass 的渲染通道,其中包含阴影贴图的管道状态和 texture 属性。
➤ 打开 Pipelines.swift,并创建一个新方法来创建管道状态对象:

static func createShadowPSO() -> MTLRenderPipelineState {let vertexFunction =Renderer.library?.makeFunction(name: "vertex_depth")let pipelineDescriptor = MTLRenderPipelineDescriptor()pipelineDescriptor.vertexFunction = vertexFunctionpipelineDescriptor.colorAttachments[0].pixelFormat = .invalidpipelineDescriptor.depthAttachmentPixelFormat = .depth32FloatpipelineDescriptor.vertexDescriptor = .defaultLayoutreturn createPSO(descriptor: pipelineDescriptor)
}

 在这里,您可以创建一个没有颜色附件或片段函数的管线状态。您只对阴影的深度信息感兴趣,而不是颜色信息 - 因此,将颜色附件像素格式设置为无效。您仍然会渲染模型,因此您仍然需要保留顶点描述符,并在顶点函数中转换所有模型的顶点。
➤打开Shadowrenderpass.swift,创建初始化器:

init() {pipelineState =PipelineStates.createShadowPSO()shadowTexture = Self.makeTexture(size: CGSize(width: 2048,height: 2048),pixelFormat: .depth32Float,label: "Shadow Depth Texture")
}


该代码初始化管道状态对象,并使用所需像素格式构建深度纹理。与其他需要匹配视图尺寸的渲染通道不同,阴影贴图通常采用正方形尺寸以适配光源的立方体正交投影相机,因此当窗口尺寸变化时无需重新调整纹理大小。该分辨率应与您的游戏资源预算允许产生更清晰的阴影一样多。

2.声明和绘制渲染通道

➤在Game group中,打开Renderer.swift,并将新的渲染通道属性添加到渲染器:
var ShadowrenderPass:ShadowrenderPass
➤在init(MetalView:options :)中,在调用super.init()之前初始化渲染通道:
Shadowrenderpass = Shadowrenderpass()
➤在mtkView(_:drawableSizeWillChange:),添加:

shadowRenderPass.resize(view: view, size: size)

目前,您尚未调整ShadowRenderPass中的任何纹理,但是由于您已经创建了resize(view:size:)作为遵循RenderPass协议的必需方法,并且您可以稍后添加纹理,因此您应该在此处调用该方法。

➤ 在 draw(scene:in:) 中,在updateUniforms(scene: scene)后,添加:

shadowRenderPass.draw(commandBuffer: commandBuffer,scene: scene,uniforms: uniforms,params: params)

此代码执行渲染通道。

3. 设置 Render Pass 绘制代码

➤ 打开 ShadowRenderPass.swift,并将以下代码添加到draw(commandBuffer:scene:uniforms:params:):

guard let descriptor = descriptor else { return }
descriptor.depthAttachment.texture = shadowTexture
descriptor.depthAttachment.loadAction = .clear
descriptor.depthAttachment.storeAction = .store
guard let renderEncoder =commandBuffer.makeRenderCommandEncoder(descriptor: descriptor)
else {
return
}
renderEncoder.label = "Shadow Encoder"
renderEncoder.setDepthStencilState(depthStencilState)
renderEncoder.setRenderPipelineState(pipelineState)
for model in scene.models {renderEncoder.pushDebugGroup(model.name)model.render(encoder: renderEncoder,uniforms: uniforms,params: params)renderEncoder.popDebugGroup()
}
renderEncoder.endEncoding()

在这里,您可以在描述符的深度附件上设置深度附件纹理。GPU 将在加载纹理时清除纹理并将其存储,以便后续渲染通道可以使用它。然后,使用描述符创建渲染命令编码器,并照常渲染场景。
使用 renderEncoder.pushDebugGroup(_:) 和 renderEncoder.popDebugGroup() 包围模型渲染,它们在 GPU 工作负载捕获时将渲染命令收集到分组中。现在,您可以更轻松地调试正在发生的事情。

4. 设置 Light Camera

在阴影通道期间,您将从太阳的角度进行渲染,因此您需要新的摄像机和一些新的着色器矩阵。
➤ 在 Common.h 的 Shaders 组中,将这些属性添加到 Uniforms:

matrix_float4x4 shadowProjectionMatrix;
matrix_float4x4 shadowViewMatrix;

在这里,您持有阳光的投影和视图矩阵。

➤ 打开 Renderer.swift,并向 Renderer 添加一个新属性:
   var shadowCamera = OrthographicCamera()
在这里,您将创建一个正交相机。您之前在第 9 章 “导航 3D 场景”中使用了正交摄像机从上方渲染场景。因为阳光是平行光,所以这是由阳光引起的阴影的正确投影类型。但是,如果希望聚光灯产生阴影,则应使用视野与聚光灯的锥体角度相匹配的透视摄像机。
➤ 在 updateUniforms(scene:) 的末尾添加以下代码:

shadowCamera.viewSize = 16
shadowCamera.far = 16
let sun = scene.lighting.lights[0]
shadowCamera.position = sun.position

使用此代码,您可以设置立方体视图体积为 16 个单位的正交相机。
➤ 继续编写更多代码:

uniforms.shadowProjectionMatrix = shadowCamera.projectionMatrix
uniforms.shadowViewMatrix = float4x4(eye: sun.position,center: .zero,up: [0, 1, 0])

shadowViewMatrix 是一个 lookAt 矩阵,可确保太阳注视场景的中心。float4x4(eye:center:up) 在 MathLibrary.swift 中定义。它采用摄像机的位置、摄像机应注视的点以及摄像机的上方向矢量。此矩阵通过提供这些参数来旋转相机以查看目标。

注意:这是一个有用的调试提示。在 updateUniforms(scene:) 结束时,临时将 uniforms.viewMatrix 设置为 uniforms.shadowViewMatrix,将 uniforms.projectionMatrix 设置为 uniforms.shadowProjectionMatrix。开发人员通常会弄错阴影矩阵,通过光线可视化场景渲染非常有用。

5. 创建 Shader 函数

你可能已经注意到,当你在 Pipelines.swift 中设置阴影管道状态对象时,它引用了一个名为 vertex_depth 的着色器函数,该函数尚不存在。

➤ 在着色器group中,使用 Metal 文件模板,创建一个名为 Shadow.metal 的新文件。请务必检查target。

➤ 将以下代码添加到新文件中:

#import "Common.h"
struct VertexIn {float4 position [[attribute(0)]];
};
vertex float4vertex_depth(const VertexIn in [[stage_in]],constant Uniforms &uniforms [[buffer(UniformsBuffer)]])
{matrix_float4x4 mvp =uniforms.shadowProjectionMatrix * uniforms.shadowViewMatrix* uniforms.modelMatrix;return mvp * in.position;
}

此代码接收顶点位置,通过您在 Renderer 中设置的光源投影和视图矩阵对其进行转换,并返回转换后的位置。
➤ 构建并运行应用程序。


 这看起来不错,但阴影在哪里?
➤ 捕获 GPU 工作负载并检查帧捕获。
您当前未转发 Shadow Encoder Pass 的结果。

➤ 双击阴影编码器传递纹理结果,以在其他资源面板中显示纹理。

这是从光源位置渲染的场景。您使用了阴影管道状态,并将其配置为没有片段着色器,因此此处根本不处理颜色信息,而是纯粹的深度。较亮的颜色距离较远,而较暗的颜色较接近。

主通道

现在,您已将阴影贴图保存到纹理中,只需将其发送到主通道,即可在片段函数的光照计算中使用该纹理。
➤ 打开 ForwardRenderPass.swift,并添加一个新属性:

weak var shadowTexture: MTLTexture?

➤ 在 draw(commandBuffer:scene:uniforms:params:) 中,在模型渲染 for 循环之前,添加:

renderEncoder.setFragmentTexture(shadowTexture, index: 15)

传入阴影纹理并将其发送到 GPU。
➤ 打开 Renderer.swift,在绘制前向渲染通道之前,将这段代码添加到 draw(scene:in:) 中:

forwardRenderPass.shadowTexture = shadowRenderPass.shadowTexture

将阴影纹理从上一个阴影通道传递到前向渲染通道。
➤ 在 Shaders 组中,打开 ShaderDefs.h,并为 VertexOut 添加新成员:float4 shadowPosition;
这保存了阴影矩阵转换的顶点位置。
➤ 打开 Vertex.metal,并在创建时将这一行添加到 vertex_main:

 .shadowPosition =uniforms.shadowProjectionMatrix * uniforms.shadowViewMatrix* uniforms.modelMatrix * in.position

每个顶点都持有两个变换的位置。一个从摄像机的视角在场景中变换,另一个从灯光的视角变换。您将能够将阴影位置与阴影贴图中的片段进行比较。
➤ 打开 Fragment.metal。

此文件是进行光照的地方,因此其余的阴影工作将发生在fragment_main。

➤ 首先,在 aoTexture 后面再添加一个函数参数:
depth2d<float> shadowTexture [[texture(15)]]
与您过去使用的纹理不同,这些纹理具有 texture2d 类型,Depth 纹理的 texture type 为 depth2d。

➤ 在fragment_main结尾,在返回之前,添加:

// shadow calculation
// 1
float3 shadowPosition= in.shadowPosition.xyz / in.shadowPosition.w;
// 2
float2 xy = shadowPosition.xy;
xy = xy * 0.5 + 0.5;
xy.y = 1 - xy.y;
xy = saturate(xy);
// 3
constexpr sampler s(coord::normalized, filter::linear,address::clamp_to_edge,compare_func:: less);
float shadow_sample = shadowTexture.sample(s, xy);
// 4
if (shadowPosition.z > shadow_sample) {diffuseColor *= 0.5;
}

下面是一个代码分解:
1. in.shadowPosition 表示从光源的角度来看顶点的位置。当您从光源的角度渲染时,GPU 在将片段写入阴影纹理之前执行透视分割。此处将 xyz 除以 w 将匹配相同的透视分割,以便您可以将当前样本的深度值与阴影纹理中的深度值进行比较。
2. 从阴影位置确定一个坐标对,以用作阴影纹理上的屏幕空间像素定位器。然后,将坐标从 [-1, 1] 重新缩放为 [0, 1] 以匹配 uv 空间。最后,您将 Y 坐标反转,因为它是倒置的。
3. 创建一个用于阴影纹理的采样器,并在您刚刚创建的坐标处对纹理进行采样。获取当前处理像素的深度值。您创建一个新的采样器,因为在函数顶部初始化的 textureSampler 会重复纹理(如果它是从边缘采样的)。稍后尝试使用 textureSampler 查看场景后面重复的额外阴影。
4. 使深度大于纹理中存储的阴影值的像素的漫反射颜色变暗。例如,如果 shadowPosition.z 为 0.5,shadow_sample 存储的深度纹理为 0.2,则从太阳的角度来看,当前片段比存储的片段更远。由于太阳看不到片段,所以它在阴影中。

➤ 构建并运行应用程序,您最终会看到带有阴影的模型。

影子痤疮

在上图中,当太阳旋转时,您会注意到很多闪烁。这称为阴影痤疮或表面痤疮。由于缺少浮点精度,表面是自阴影的,其中采样的纹素与计算值不匹配。
您可以通过向阴影纹理添加偏差,增加 z 值,从而使存储的片段更接近来缓解这种情况。
➤ 将上面 // 4 处的条件测试更改为:
if (shadowPosition.z > shadow_sample + 0.001) {
➤ 构建并运行应用程序。

表面痤疮现在已经消失了,当太阳围绕场景旋转时,您会有清晰的阴影。

发现问题

看看之前的渲染,你会看到一个问题。实际上,有两个问题。平面上的大片深灰色区域似乎处于阴影中,但不应如此。

如果你捕捉到这个场景,这是该太阳位置的深度纹理:

图像的下四分之一是白色的,这意味着该位置的深度是最远的。光的正交相机切断了平面的那部分,使它看起来好像在阴影中。
第二个问题是在读取阴影贴图。
➤打开 Fragment.metal。在 fragment_main 中,在 xy = saturate(xy); 之前,添加:

 if (xy.x < 0.0 || xy.x > 1.0 || xy.y < 0.0 || xy.y > 1.0) {return float4(1, 0, 0, 1);
}

xy 纹理坐标应该从 0 到 1 来在纹理上。所以如果坐标不在纹理上,则返回红色。

➤ 构建并运行应用程序。

红色区域不在深度纹理中。
您可以通过设置灯光的正交摄像机来封闭场景摄像机捕捉的所有内容,从而解决这两个问题。

可视化问题

在 Utility 组中,DebugCameraFrustum.swift 将通过渲染各种摄像机视锥体的线框来帮助您可视化此问题。运行应用程序时,您可以按各种键进行调试:
• 1:场景的前视图。
• 2:太阳围绕场景旋转的默认视图。
• 3:渲染场景摄像机视锥体的线框。
• 4:渲染轻型摄像机视锥体的线框。
• 5:渲染场景摄像机边界球体的线框。

此关键代码位于 GameScene 的 update(deltaTime:) 中。

➤ 打开 ForwardRenderPass.swift,并将以下内容添加到 draw(commandBuffer:scene:uniforms:params:) 的末尾,在 renderEncoder.endEncoding() 之前:

DebugCameraFrustum.draw(encoder: renderEncoder,scene: scene,uniforms: uniforms)


此代码设置调试代码,以便上述按键正常工作。

➤ 打开 GameScene.swift,然后在 init() 的顶部添加:
camera.far = 5
摄像机远平面的默认值为 100,很难可视化。5 的远平面非常接近且易于可视化,但会暂时切断很多场景。

➤ 构建并运行应用程序。

您可以看到,由于较近的远平面,大部分场景都丢失了。

➤ 按字母上方键盘上的数字 3 键。
在这里,您将暂停太阳的旋转并创建一个新的透视弧球摄像机,该摄像机从远处俯视场景,并以蓝色线框渲染原始透视场景摄像机视锥体。

使用鼠标或触控板拖动场景以旋转并检查它。您将看到第三棵树位于蓝色视锥体之外,因此未渲染。您还将看到阴影纹理覆盖场景的位置。红色区域位于阴影纹理之外。

➤ 按数字 4 键。
正交灯光的 Camera View Volume 线框显示为黄色。

光源的视图体积的一条边来自灰色平面的角。这是光线的视锥体无法到达的地方,并在阴影贴图纹理上显示为白色。

➤ 在 GameScene 中,将 camera.far = 5 更改为:camera.far = 10

➤ 构建并运行应用程序。当您看到一块红色平面时,按 3 键,然后按 4 键。

旋转场景,您将看到蓝色线框延伸到红色区域。黄色线框应该包含该区域,但目前没有。
➤ 按数字 5 键。
这将显示一个包围场景摄像机视锥体的白色边界球体。

光体积应包含白色边界球体,以获得最佳阴影。

解决问题

➤ 在 Game 组中,打开 ShadowCamera.swift。此文件包含用于计算摄像机视锥体拐角的各种方法。createShadowCamera(using:lightPosition:) 创建一个包含指定相机的正交相机。
➤ 打开 Renderer.swift。在 updateUniforms(scene:) 中,将 shadowCamera.viewSize = 16 

let sun = scene.lighting.lights[0]
shadowCamera = OrthographicCamera.createShadowCamera(using: scene.camera,lightPosition: sun.position)
uniforms.shadowProjectionMatrix = shadowCamera.projectionMatrix
uniforms.shadowViewMatrix = float4x4(eye: shadowCamera.position,center: shadowCamera.center,up: [0, 1, 0])

在这里,您将创建一个正交光源摄像机,其视图体积完全包裹 scene.camera 的视锥体。
➤ 构建并运行应用程序。

现在,Light Camera Volume 将整个场景封闭起来,因此您不会看到任何红色错误或错误的灰色补丁。
➤ 打开 GameScene.swift。在 init() 中,删除:camera.far = 10
删除对 camera.far 的分配,这会将默认 camera.far 恢复为 100。
➤ 构建并运行应用程序。
由于光照视图体积巨大,阴影非常块状。下图在右侧显示渲染的阴影纹理。你几乎看不到任何细节。

您可以将 5 或 20 更改为 5 或 20 并捕获 GPU 工作负载以比较阴影纹理质量。
在这种情况下,作为游戏设计师,您必须决定阴影质量。最好的结果是,在离摄像机较近的阴影上使用远值 5,对较远的阴影使用远值 20,因为分辨率并不重要。 

级联的阴影贴图

现代游戏使用一种称为级联的阴影贴图的技术来帮助平衡性能和阴影深度。在第8章“纹理”中,您了解了MIP地图,GPU使用的不同尺寸的纹理取决于与摄像机的距离。级联的影子地图采用了类似的想法。
使用级联的阴影贴图,您将场景渲染为深度纹理阵列的几个阴影贴图,并使用近距离平面的不同平面。如您所见,较小的远值会产生较小的光量,从而产生更详细的阴影贴图。您可以从阴影贴图中采样阴影,较小的光量为靠近场景摄像机的片段采样。
更远的地方,您不需要太多的精度,因此您可以从更大的浅色片段中采样阴影,从而吸收更多场景。缺点是,您必须为每个阴影贴图多次渲染场景。
阴影可能需要大量的计算和处理时间。您必须决定给他们多少帧时间限额。在本章的资源文件夹中,references.markdown包含一些有关改善阴影的通用技术的文章。

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

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

相关文章

Matplotlib:数据可视化的艺术与科学

引言&#xff1a;让数据开口说话 在数据分析与机器学习领域&#xff0c;可视化是理解数据的重要桥梁。Matplotlib 作为 Python 最流行的绘图库&#xff0c;提供了从简单折线图到复杂 3D 图表的完整解决方案。本文将通过实际案例&#xff0c;带您从基础绘图到高级定制全面掌握 …

Python数据可视化-第4章-图表样式的美化

环境 开发工具 VSCode库的版本 numpy1.26.4 matplotlib3.10.1 ipympl0.9.7教材 本书为《Python数据可视化》一书的配套内容&#xff0c;本章为第4章 图表样式的美化 本章主要介绍了图表样式的美化&#xff0c;包括图表样式概述、使用颜色、选择线型、添加数据标记、设置字体…

嵌入式海思Hi3861连接华为物联网平台操作方法

1.1 实验目的 快速演示 1、认识轻量级HarmonyOS——LiteOS-M 2、初步掌握华为云物联网平台的使用 3、快速驱动海思Hi3861 WIFI芯片,连接互联网并登录物联网平台

如何在Redis容量限制下保持热点数据

如何在Redis容量限制下保持热点数据 当数据库有100万条数据但Redis只能保存10万条时,需要智能的策略来确保Redis中存储的都是最常访问的热点数据。以下是几种有效的解决方案: 一、内存淘汰策略 Redis提供了多种内存淘汰机制,当内存不足时会自动删除部分数据: 策略命令/配…

cv2.fillPoly()和cv2.polylines()

参数解释 cv2.fillPoly() 和 cv2.polylines() 都是 OpenCV 的函数。功能是绘制多边形&#xff0c;cv2.fillPoly()可绘制实心多边形&#xff0c; cv2.polylines() 可绘制空心多边形 cv2.fillPoly()用途&#xff1a;提取ROI 可在黑色图像上&#xff0c;填充白色&#xff0c;作为…

数据库--SQL

SQL&#xff1a;Structured Query Language&#xff0c;结构化查询语言 SQL是用于管理关系型数据库并对其中的数据进行一系列操作&#xff08;包括数据插入、查询、修改删除&#xff09;的一种语言 分类&#xff1a;数据定义语言DDL、数据操纵语言DML、数据控制语言DCL、事务处…

【python】速通笔记

Python学习路径 - 从零基础到入门 环境搭建 安装Python Windows: 从官网下载安装包 https://www.python.org/downloads/Mac/Linux: 通常已预装&#xff0c;可通过终端输入python3 --version检查 配置开发环境 推荐使用VS Code或PyCharm作为代码编辑器安装Python扩展插件创建第…

批量删除git本地分支和远程分支命令

1、按照关键词开头匹配删除远程分支 git branch -r | grep "origin/feature/develop-1"| sed s/origin\///g | xargs -n 1 git push origin --delete git branch -r 列出所有远端分支。 grep "origin/feature/develop-1" 模糊匹配分支名称包含"orig…

上市电子制造企业如何实现合规的质量文件管理?

浙江洁美电子科技股份有限公司成立于2001年&#xff0c;是一家专业为片式电子元器件(被动元件、分立器件、集成电路及LED)配套生产电子薄型载带、上下胶带、离型膜、流延膜等产品的国家高新技术企业&#xff0c;主要产品有分切纸带、打孔经带、压孔纸带、上下胶带、塑料载带及其…

leetcode数组-有序数组的平方

题目 题目链接&#xff1a;https://leetcode.cn/problems/squares-of-a-sorted-array/ 给你一个按 非递减顺序 排序的整数数组 nums&#xff0c;返回 每个数字的平方 组成的新数组&#xff0c;要求也按 非递减顺序 排序。 输入&#xff1a;nums [-4,-1,0,3,10] 输出&#xff…

基于微信小程序的医院挂号预约系统设计与实现

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

密码学基础——DES算法

前面的密码学基础——密码学文章中介绍了密码学相关的概念&#xff0c;其中简要地对称密码体制(也叫单钥密码体制、秘密密钥体制&#xff09;进行了解释&#xff0c;我们可以知道单钥体制的加密密钥和解密密钥相同&#xff0c;单钥密码分为流密码和分组密码。 流密码&#xff0…

Redis分布式锁详解

Redis分布式锁详解 分布式锁是在分布式系统中实现互斥访问共享资源的重要机制。Redis因其高性能和原子性操作特性&#xff0c;常被用来实现分布式锁。 一、基础实现方案 1. SETNX EXPIRE方案&#xff08;基本版&#xff09; # 加锁 SETNX lock_key unique_value # 设置唯…

创建Linux虚拟环境并远程连接,finalshell自定义壁纸

安装VMware 这里不多赘述。 挂载Linux系统 1). 打开Vmware虚拟机&#xff0c;打开 编辑 -> 虚拟网络编辑器(N) 选择 NAT模式&#xff0c;然后选择右下角的 更改设置。 设置子网IP为 192.168.100.0&#xff0c;然后选择 应用 -> 确定。 解压 CentOS7-1.zip 到一个比较大…

podman和与docker的比较 及podman使用

Podman 与 Docker 的比较和区别 架构差异 Docker&#xff1a;采用客户端 - 服务器&#xff08;C/S&#xff09;架构&#xff0c;有一个以 root 权限运行的守护进程 dockerd 来管理容器的生命周期。客户端&#xff08;docker 命令行工具&#xff09;与守护进程进行通信&#x…

【Easylive】HttpServletRequest、HttpServletResponse、HttpSession 介绍

【Easylive】项目常见问题解答&#xff08;自用&持续更新中…&#xff09; 汇总版 这三个是 Java Web 开发&#xff08;Servlet/JSP&#xff09;的核心接口&#xff0c;用于处理 HTTP 请求和响应 以及 用户会话管理。它们在 Spring MVC&#xff08;Controller&#xff09;中…

Markdown使用说明

以下是Markdown基础使用教程及分割线展示方法&#xff1a; &#x1f4dd; Markdown基础使用教程 1. 标题 # 一级标题 ## 二级标题 ### 三级标题2. 文本样式 *斜体* 或 _斜体_ **加粗** 或 __加粗__ ***加粗斜体*** 或 ___加粗斜体___ ~~删除线~~3. 列表 - 无序列表项 * 另一…

Jmeter的压测使用

Jmeter基础功能回顾 一、创建Jmeter脚本 1、录制新建 &#xff08;1&#xff09;适用群体&#xff1a;初学者 2、手动创建 &#xff08;1&#xff09;需要了解Jmeter的常用组件 元件&#xff1a;多个类似功能组件的容器&#xff08;类似于类&#xff09; 各元件作用 组件…

【rabbitmq基础】

RabbitMq基础 1.概念2.数据隔离3.使用控制台向mq传递消息1.创建两个队列-“测试队列”&#xff0c;“测试队列2”2.创建一个交换机-"测试交换机"3.测试发送消息3.1让交换机和队列进行绑定3.2发送消息3.3查看消息 4.创建虚拟主机5.java使用rabbitmq5.1 发送消息5.2 消…

加固计算机厂家 | 工业加固笔记本电脑厂家

北京鲁成伟业科技发展有限公司&#xff08;以下简称“鲁成伟业”&#xff09;成立于2005年&#xff0c;是集研发、生产、销售与服务于一体的高新技术企业&#xff0c;专注于加固计算机、工业加固笔记本电脑及特种计算机的研发与制造。凭借20年的技术积累与行业深耕&#xff0c;…