Learn SRP 01

news2024/11/25 12:48:37

学习链接:Custom Render Pipeline (catlikecoding.com)

使用Unity版本:Unity 2022.3.5f1

1.A new Render Pipeline

1.1Project Setup

创建一个默认的3D项目,项目打开后可以到默认的包管理器删掉所有不需要的包,我们只使用Unity UI这个包来绘制UI,因此可以保留这个包

另外,在项目设置里将颜色空间设置为线性空间

接着在场景中放置一些物体,分别使用 standard, unlit opaque and transparent 材质. The Unlit/Transparent shader only works with a texture, so here is a UV sphere map for that.

红色的立方体使用 Standard shader, 绿色和黄色的立方体使用Unlit/Color shader. 蓝色的球体使用 Standard shader with Rendering Mode set to Transparent, 白色的球体使用 Unlit/Transparent shader.

1.2Pipeline Asset

到目前位置,Unity还是使用的默认的渲染管线。我们首先需要创建一个可编程渲染管线资产并使用它来替换默认渲染管线。我们将使用和Unity通用渲染管线(URP)相似的文件结构。创建名为Custom RP资源文件夹和一个名为Runtime的子文件夹。在这里放入一个新的C#脚本名字为CustomRenderPipelineAsset。

这个资产类型必须继承自RenderPiplineAsset,它在UnityEngine.Rendering命名空间下。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
public class CustomRenderPipelineAsset : RenderPipelineAsset
{
    protected override RenderPipeline CreatePipeline()
    {
        return null;
    }


}

渲染管线资产的主要目的是为Unity提供一种获取负责渲染的管道对象实例的方法。资产本身只是一个句柄和一个存储设置的地方。我们暂时还没有任何设置,所以我们要做的只是给予Unity一个获取我们渲染管线实例的方式。我们通过重写抽象方法CreatePipeline返回一个RenderPipeline的实例来实现。但是我们现在还没有定义一个自定义渲染管线,所以我们先返回空(null)。

CreatePipeline方法是由protected限制符定义的,这意味着类本身和继承自RenderPipelineAsset的类才可以使用此方法。

我们需要为我们的项目添加一个这个类的资产。为了实现这个功能我们为CustomRenderPipelineAsset添加一个CreateAssetMenu的特性。

这将在Asset/Create按钮下添加一个入口。为了保持整洁让我们把它放到Rendering子按钮下。我们通过menuName属性设置为Rendering/Custom Render Pipeline来实现。此属性可以直接设置在属性类型后面的圆括号内。

使用这个新按钮来添加资产到项目中,然后到Graphics 项目设置中,在Scriptable Render Pipeline Settings面板选择设置这个资产。

替换默认的渲染管线会造成一些事情发生变化。首先在graphics settings信息面板中的一些选项将消失。第二,我们禁用了默认的渲染管线,但没有提供有效的替换,因此不会再呈现任何内容。Game视图,场景视图,材质预览将不再起作用。如果你打开frame debugger -Window / Analysis / Frame Debugger并开启它,你会看到没有任何东西绘制到game窗口。

1.3Render Pipeline Instance

创建一个CustomRenderPipeline类,把它放入和CustomRenderPipelineAsset相同的文件夹。这个类必须继承自RenderPipeline,它将被使用在CustomRenderPipelineAsset中创建并返回的渲染管线的实例。

RenderPipeline定义了一个受保护的抽象方法Render,我们必须重写它来实现一个具体的管线。它有两个参数:一个ScriptableRenderContext和一个Camera数组。暂时将方法置空。

using UnityEngine;
using UnityEngine.Rendering;

public class CustomRenderPipeline : RenderPipeline
{
    protected override void Render(ScriptableRenderContext context, Camera[] cameras)
    {
       
    }
}

使CustomRenderPipelineAsset.CreatePipeline函数返回一个CustomeRenderPipeline的实例。这将为我们提供一个有效且功能强大的管线,尽管它还没有呈现任何内容。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
public class CustomRenderPipelineAsset : RenderPipelineAsset
{
    protected override RenderPipeline CreatePipeline()
    {
        return new CustomRenderPipeline();
    }


}

2.Rendering

在渲染管线实例中Unity每帧都会调用Render函数。这个函数传递了一个上下文结构来提供一个和原生引擎的连接,我们可以使用它来进行渲染。因为场景中可能有多个激活的摄像机,所以这个函数同时也传递一组摄像机的信息。根据渲染的顺序对摄像机进行渲染是渲染管线的责任。

2.1Camera Renderer

每一个摄像机的渲染都是独立的。因此与其让CustomRenderPipeline渲染所有摄像机,不如创建一个全新的类来专门负责单独摄像机的渲染。这个类的名字叫做CameraRenderer,给它创建一个公开Render函数包括contextcamera两个参数。为了方便起见让我们保存这些参数到字段中。

using UnityEngine;
using UnityEngine.Rendering;

public class CameraRenderer
{

	ScriptableRenderContext context;

	Camera camera;

	public void Render(ScriptableRenderContext context, Camera camera)
	{
		this.context = context;
		this.camera = camera;
	}
}

CustomRenderPipeline创建的时候创建一个CameraRenderer的实例,然后在一个循环中使用它来渲染所以摄像机。

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class CustomRenderPipeline : RenderPipeline
{
    CameraRenderer renderer = new CameraRenderer();
	protected override void Render(ScriptableRenderContext context, Camera[] cameras)
	{ 

	}

	protected override void Render(ScriptableRenderContext context, List<Camera> cameras)
	{
		for (int i = 0; i < cameras.Count; i++)
		{
			renderer.Render(context, cameras[i]);
		}
	}
}

我们的摄像机渲染逻辑大致相当于可编程渲染管线中的通用渲染管线。这种方式使得将来每个摄像机支持不同的渲染方式变得更加简单,例如第一人称视角,3D地图覆盖,或者是正向渲染和延迟渲染。但是现在我们将使用同样的方式渲染所有摄像机。

2.2Drawing the Skybox

CameraRenderer.Render的工作是绘制所有相机可以看到的几何体。为了使代码清晰,我们在单独的一个函数DrawVisibleGeometry内执行此逻辑。我们先通过使用contextDrawSkybox函数,并传递camera的作为参数来绘制默认的天空盒。

但这还不能使天空盒显示。因为这些我们发布到context的命令只是一些缓冲。我们必须使用contextSubmit方法来提交这些排队等待执行指令。让我们在执行完DrawVisibleGemometry后,在一个独立的Submit函数执行提交。

	public void Render(ScriptableRenderContext context, Camera camera)
	{
		this.context = context;
		this.camera = camera;

		DrawVisibleGeometry();
		Submit();
	}

	void Submit()
	{
		context.Submit();
	}

	void DrawVisibleGeometry()
	{
		context.DrawSkybox(camera);
	}

天空盒最终在gamescene视图中都显示了。当你激活frame debugger的时候你也可以看到一个天空盒的入口。它以列表的形式的呈现在Camera.RenderSkybox中,在它下面有一个唯一的选项DrawMesh,这代表了实际发生的draw call。这个列表对应游戏窗口的渲染情况。FrameDebgger不会提供其他窗口的渲染情况。

请注意,当前摄像机的方向并不会影响天空盒的绘制结果。我们传递cameraDrawSkybox函数,只用于通过控制摄像机的清除标志来确认天空盒是否应该被绘制。

为了正确的渲染天空盒-整个场景-我们需要设置视图-投影矩阵。这个转换矩阵包含了摄像机的位置和方向-视图矩阵-摄像机的正交和透视投影-投影矩阵。它被熟知是作为着色器中的绘制几何体的时候使用的着色器属性之一untiy_MatrixVP。你可以在frame debugger中选中一个drawcall,在ShaderProperties部分观察这个矩阵。

目前unity_MatrixVP矩阵总是相同的。我们需要通过SetupCameraProperties方法将摄像机的属性应用于context上下文。它设置了矩阵和其他一些摄像机的属性。我们在DrawVisibleGeometry函数之前的单独函数Setup中执行这些操作。 

执行完这一步以后天空盒就正确对齐了,并且会随着摄像机的朝向而改变

2.3Command Buffers

上下文将延迟渲染直到我们提交它。在这之前,我们将对其进行配置并添加命令以供后面执行。一些任务-像绘制天空盒-可以通过专用方法发布,但是其他的指令必须通过单独的命令缓冲区间接发布。我们需要这样的缓冲区来绘制场景中的其他几何体。

为了获得一个缓冲区,我们必须创建一个新的CommandBuffer实例对象。我们只需要一个缓冲区,所以我们在默认情况下为CameraRender创建一个缓冲区,并将它的引用存储在字段中。同时我们给缓冲区一个名字,这样我们就可以在frame debugger中识别它。那就叫做 Render Camera吧。

	const string bufferName = "Render Camera";

	CommandBuffer buffer = new CommandBuffer {
		name = bufferName
	};

对象的实例化语法如何工作?

就像如果我们代码为buffer.name = bufferName;在构造方法执行完毕后作为一段分离的部分。但是当创建一个新对象的时候,也可以添加一个代码块到构造函数中执行。然后你可以设置对象的字段和属性而不需要显示的调用对象的引用。这明确的规定了实例只有在设置完这些属性和字段之后才可以被使用。除此之外它使只使用一个语句进行初始化变为可能-例如,字段的初始化,我们在这里使用它,从而不在需要有许多参数变量的构造函数。

请注意我们省略了空参数列表的构造函数的执行,这种语法也是被允许的。

我们可以使用命令缓冲区注入到采样分析器,他将同时显示在profiler中和framedebugger中。这是通过在适当的点调用BeginSampleEndSample来完成的,在我们的案例中它们在Setup函数和Submit函数中。两个方法都必须提供相同的采样名称,我们将使用缓冲区的名字。

	void Setup () {
		buffer.BeginSample(bufferName);
		context.SetupCameraProperties(camera);
	}

	void Submit () {
		buffer.EndSample(bufferName);
		context.Submit();
	}

为了执行缓冲区,我们将缓冲区作为参数执行contextExecuteCommandBuffer方法。它从缓冲区复制命令,但并没有清除缓冲区,如果我们想重用它,我们必须显示的清除它。因为执行和清除总是一起完成的,所以添加一个同时执行和清除的方法会很方便。

Camera.RenderSkyBox采样嵌套在RenderCamera内部。

2.4Clearing the Render Target

无论我们绘制什么最终都会渲染到摄像机的渲染目标中,默认情况下是帧缓冲区,但也可以是渲染纹理(RT)。之前被绘制到目标上的东西依然存在,这可能会干扰我们现在渲染的图像。为了保证正确的渲染,我们必须清除渲染目标以去除其旧内容。这是通过调用命令缓冲区上的ClearRenderTarget来完成的,它在Setup方法中执行。

CommandBuffer.ClearRenderTarget函数至少需要三个参数。前两个参数表示是否清除深度缓冲和颜色缓冲数据,我们设置为true。第三个参数为清除后的颜色(可以理解为替换的颜色),我们选择Color.clear。

	void Setup () {
		buffer.BeginSample(bufferName);
		buffer.ClearRenderTarget(true, true, Color.clear);
		ExecuteBuffer();
		context.SetupCameraProperties(camera);
	}

FrameDebugger现在显示了一个DrawGL入口用于清理行为,嵌套在RenderCamera的内部。之所以发生这种情况是因为ClearRenderTarget在示例中放置在开始采样之后,会被程序认为是开始采样之后的操作。我们可以在开始采样前,清除多余的嵌套内容。这样做的结果是两个相邻的"Render Camera"采样将可以合并。 

	void Setup()
	{
		buffer.ClearRenderTarget(true, true, Color.clear);

		buffer.BeginSample(bufferName);
		ExecuteBuffer();
		context.SetupCameraProperties(camera);
	}

Draw GL入口表示使用Hidden/InternalClear着色器来绘制全屏四边形,并写入渲染目标,但这不是清除目标的最有效方法。之所以使用这种方法,是因为我们在设置相机属性之前先进行了清理。如果我们交换这两个步骤的顺序,便可以更快速清除。

这里DrawGL是通过着色器渲染之后写入之后渲染目标,而clear方法是直接修改三个缓冲的值,很明显后者效率要更好一些,现在我们看到Clear(color+Z+stencil),这表示颜色,深度和模板缓冲区都被清除了。

2.5Culling

现在可以看见天空盒了,但是仍然看不见物体。我们只需要渲染在摄像机可视范围内的可视对象,而不是所有的对象。我们首先从场景中所有具有渲染组件的对象开始,然后剔除掉那些不在摄像机视椎体之内的对象。

要想弄清楚哪些是可以被剔除的,我们需要通过使用ScriptableCullingParameters结构体来跟踪多个摄像机的设置和矩阵。我们可以调用cameraTryGetCullingParameters函数来代替自己填充结构体的数据。这个函数将返回这些参数是否可以成功获取并返回,对于错误的摄像机参数它有可能返回失败。为了获取这个参数数据我们必须将它作为一个输出参数,通过在变量前面添加out关键字。我们在一个独立的Cull函数内执行这些操作,它将返回成功或者失败。

	bool Cull () {
		ScriptableCullingParameters p
		if (camera.TryGetCullingParameters(out p)) {
			return true;
		}
		return false;
	}

为什么我们需要写out关键字?

当一个结构体参数被定义为一个输出参数的时候它的行为像是一个对象引用,指向了该参数所在的内存堆栈上的位置。当方法改变这个参数的时候它将影响这个值对象,而不是创建一个拷贝。

out关键字告诉我们该方法负责正确设置参数,替换先前的值。

Try-get方法是一个普遍的方式来判断执行的成功或失败,并返回一个结果。

当用作输出参数时,可以在参数列表中内联变量声明,所以让我们这样做吧。

	bool Cull()
	{
		//ScriptableCullingParameters p
		if (camera.TryGetCullingParameters(out ScriptableCullingParameters p))
		{
			return true;
		}
		return false;
	}

Render函数中Setup函数之前执行Cull函数,如果执行函数失败将返回。

	public void Render(ScriptableRenderContext context, Camera camera)
	{
		this.context = context;
		this.camera = camera;

		if (!Cull())
		{
			return;
		}

		Setup();
		DrawVisibleGeometry();
		Submit();
	}

实际的剔除操作是通过执行contextCull函数来完成的,它会生成一个CullingResults的结构体。它将在Cull函数中执行如果成功会将结构保存在字段中。在这种情况下,我们必须将剔除参数作为引用参数传递,方法是在它前面写ref关键字。

	CullingResults cullingResults;

	…
	
	bool Cull () {
		if (camera.TryGetCullingParameters(out ScriptableCullingParameters p)) {
			cullingResults = context.Cull(ref p);
			return true;
		}
		return false;
	}

为什么我们必须使用ref

ref关键字的执行和out很像,不同的是方法中不需要为它初始化。谁调用该方法就需要提取对参数进行初始化。所有它一点可比被作为输入,同时也可以作为输出。

设个例子中ref被使用作为一个优化,避免了传参过程中ScriptableCullingParameters结构体的复制,这个结构体确实很大。它是一个结构体来代替一个对象时另一个优化,为了减少内存的分配。

2.6Drawing Geometry

一旦我们知道哪些物体可见,我们就可以继续渲染这些物体了。渲染这些物体需要使用剔除的结果作为参数,执行contextDrawRenderers函数来告诉管线哪些物体可以被渲染。除此之外,我们还必须提供绘制设置参数和过滤设置参数。它们都是结构体-DrawingSettingsFilteringSettings-我们将首先使用它们的默认构造函数,它们都需要通过引用进行传递。在DrawVisibleGeometry函数中绘制天空盒之前执行这些逻辑。

	void DrawVisibleGeometry()
	{
		var drawingSettings = new DrawingSettings();
		var filteringSettings = new FilteringSettings();

		context.DrawRenderers(
			cullingResults, ref drawingSettings, ref filteringSettings
		);

		context.DrawSkybox(camera);
	}

我们还是看不到任何东西,因为我们需要表明哪些类型的着色器过程(shader passes)是被允许渲染的。由于我们本节教程只支持无光照的着色器,因此我们需要获取SRPDefaultUnlit的着色器标记ID。我们可以只获取一次并将它缓存到静态字段中。

​
static ShaderTagId unlitShaderTagId = new ShaderTagId("SRPDefaultUnlit");

​

将它作为DrawingSettings构造函数的第一个参数,并创建一个SortingSettings作为第二个参数。把camera传给sortingsetting的构造函数,因为用camera来确定是使用正交排序还是使用基于距离的排序。

除此之外,我们还需要指定哪些渲染队列被允许渲染。传递RenderQueueRange.allFilteringSettings的构造函数,以便我们可以允许所有内容进行渲染。

最终代码:

	void DrawVisibleGeometry()
	{
		//决定物体绘制顺序是正交排序还是基于深度排序的配置
		var sortingSettings = new SortingSettings(camera)
		{
			criteria = SortingCriteria.CommonOpaque
		};
		//决定摄像机支持的Shader Pass和绘制顺序等的配置
		var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings);
		//决定过滤哪些Visible Objects的配置,包括支持的RenderQueue等
		var filteringSettings = new FilteringSettings(RenderQueueRange.all);
		//渲染CullingResults内的VisibleObjects
		context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);
		//添加“绘制天空盒”指令,DrawSkybox为ScriptableRenderContext下已有函数,这里就体现了为什么说Unity已经帮我们封装好了很多我们要用到的函数,SPR的画笔~
		context.DrawSkybox(camera);
	}

绘制结果:

可以看到这个透明的球的效果还有一些奇怪

分析Frame Debugger又增加了一个RenderLoop.Draw,可以知道每调用一次context.DrawRenderers就是一次renderloop

这里如果调用两次绘制的话,会自动合并到RenderLoop.Draw里

如果在中间加入一次绘制天空盒的调用,就会产生两个renderLoop.draw

接下俩就是要正确绘制透明物体。

2.7Drawing Opaque and Transparent Geometry Separately

通过Frame Debugger 可以知道目前的渲染顺序是先绘制所有unlit物体然后绘制天空盒,但是unlit物体中的透明物体渲染的时候是不会写入深度的,因此天空盒会将透明物体覆盖。所以,正确的绘制顺序应该是先绘制不透明物体,然后绘制天空盒,最后绘制透明物体。

我们可以通过切换到RenderQueueRange.opaque来排除透明物体。

然后在绘制天空盒之后,我们再次调用DrawRenderers。但是在这之前先改变渲染范围为RenderQueueRange.transparent。也要修改排序标准为SortingCriteria.CommonTransparent然后设置给DrawingSettings。这将反转透明对象的绘制顺序。

这里修改了渲染顺序为从后往前,因为这样才能体现出前后关系

绘制结果:

最终代码:

	void DrawVisibleGeometry()
	{
		//决定物体绘制顺序是正交排序还是基于深度排序的配置
		var sortingSettings = new SortingSettings(camera)
		{
			criteria = SortingCriteria.CommonOpaque
		};
		//决定摄像机支持的Shader Pass和绘制顺序等的配置
		var drawingSettings = new DrawingSettings(unlitShaderTagId, sortingSettings);
		//决定过滤哪些Visible Objects的配置,包括支持的RenderQueue等
		var filteringSettings = new FilteringSettings(RenderQueueRange.opaque);
		//渲染CullingResults内不透明的VisibleObjects
		context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);
		//添加“绘制天空盒”指令,DrawSkybox为ScriptableRenderContext下已有函数,这里就体现了为什么说Unity已经帮我们封装好了很多我们要用到的函数,SPR的画笔~
		context.DrawSkybox(camera);
		//渲染透明物体
		//设置绘制顺序为从后往前
		sortingSettings.criteria = SortingCriteria.CommonTransparent;
		//注意值类型
		drawingSettings.sortingSettings = sortingSettings;
		//过滤出RenderQueue属于Transparent的物体
		filteringSettings.renderQueueRange = RenderQueueRange.transparent;
		//绘制透明物体
		context.DrawRenderers(cullingResults, ref drawingSettings, ref filteringSettings);

	}

帧调试器结果:

可以看到是先绘制两个不透明物体,然后是天空盒,最后是三个不透明物体

目前我们已经可以渲染出所有无光照着色器的物体,但是使用其他着色器的物体并没有显示出来,下一节的目的就是将使用其他着色器的物体也显示出来。

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

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

相关文章

竞赛课第六周(树状数组的应用)

实验内容: HDU 1166 敌兵布阵【线段树】 线段树的应用 敌兵布阵 C国的死对头A国这段时间正在进行军事演习&#xff0c;所以C国间谍头子Derek和他手下Tidy又开始忙乎了。A国在海岸线沿直线布置了N个工兵营地,Derek和Tidy的任务就是要监视这些工兵营地的活动情况。由于采取…

mybatis-puls-配置日志

#日志配置 mybatis-plus.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImpl 我们所有的sql现在是不可见的&#xff0c;我们希望知道它是怎么执行的&#xff0c;所以我们必须要看日志

【示例】MySQL-SQL语句优化

前言 本文主要讲述不同SQL语句的优化策略。 SQL | DML语句 insert语句 插入数据的时候&#xff0c;改为批量插入 插入数据的时候&#xff0c;按照主键顺序插入 大批量插入数据的时候&#xff08;百万&#xff09;&#xff0c;用load指令&#xff0c;从本地文件载入&#x…

springCloud项目打包 ,maven package或install打包报错

解决思路一&#xff1a; <build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>2.3.7.RELEASE</version></plugin><plugin>&…

MySQL中的SQL高级语句[二]

使用语言 MySQL 使用工具 Navicat Premium 16 代码能力快速提升小方法&#xff0c;看完代码自己敲一遍&#xff0c;十分有用 拖动表名到查询文件中就可以直接把名字拉进来以下是使用脚本方法&#xff0c;也可以直接进行修改中括号&#xff0c;就代表可写可不写 有些地方的代…

在Ubuntu上安装Docker Compose

Docker Compose 是一个用于定义和管理Docker容器的工具&#xff0c;它使用yml来配置应用的服务、网络和卷等。特别是在定义多个容器时&#xff0c;它非常擅长定义多个容器之间的关系和依赖。 第一步&#xff1a;更新软件包 sudo apt update第二步&#xff1a;安装网络工具cur…

我企业的业务需要制作企业网站吗?11个支持的理由以及5个反对的理由!

如果你的企业经营得还不错&#xff0c;你可能会找出很多理由&#xff0c;说明为什么一个高效的网站对你来说并不那么重要。确实&#xff0c;你明白企业需要在互联网上有一定的存在感&#xff0c;但你可能并不认为一个高效的网站会对你的特定业务产生太大的影响——尤其是当你已…

无尘净化棉签:医药、化妆品及工业应用中的重要角色及防静电特性

无尘净化棉签是一种在医药、化妆品、电子光电产品、半导体、光学、磁头清洗、视频头、清洗头等领域广泛应用的清洁工具。本文探讨了无尘净化棉签在这些领域中的重要性&#xff0c;并特别关注了其防静电特性对产品质量和性能的影响。 无尘净化棉签是一种专为减少或避免产生灰尘和…

MySQL分区表(14/16)

分区表 基本概述 分区表是数据库中一种用于优化大型表数据管理和查询性能的技术。它将一个表的数据根据特定的规则或条件分割成多个部分&#xff0c;每个部分称为一个分区。每个分区可以独立于其他分区进行存储、管理和查询&#xff0c;这样可以提高数据处理的效率&#xff0…

代码随想录第38天| 509. 斐波那契数 70. 爬楼梯

理论基础 刷题大纲&#xff1a; 动态规划5步曲&#xff1a; 1、确定dp数组以及下标的含义 2、确定递推公式 3、dp数组如何初始化 4、确定遍历顺序 5、举例推导dp数组 509. 斐波那契数 509. 斐波那契数 - 力扣&#xff08;LeetCode&#xff09; 代码随想录 (programmercarl.co…

羊大师春季运动受伤应急妙招

春季运动时受伤是很常见的情况&#xff0c;特别是在户外活动中。了解一些应急妙招&#xff0c;可以帮助你在受伤时进行初步处理&#xff0c;减轻伤害。以下是几种常见运动伤害的应急处理方法&#xff1a; 扭伤和拉伤 休息&#xff1a;立刻停止运动&#xff0c;避免对受伤部位…

安达发|APS软件系统助力高端装备制造业高质量发展

APS&#xff08;Advanced Planning and Scheduling&#xff0c;高级计划与排程系统&#xff09;软件系统在高端装备制造业中的应用&#xff0c;对于推动行业高质量发展具有重要作用。以下是对这一主题的详细探讨&#xff1a; 高端装备制造业指那些技术含量高、附加值大、市场竞…

Windows 关闭占用指定端口的进程

以下示例以443端口为例&#xff0c;具体哪个端口视自己情况而定 输入命令 # 输出的最后一列就是进程号pid netstat -ano | findstr "443" 找出占用443端口的进程号(pid)&#xff08;第二列是你本机的应用占用的端口&#xff0c;看第二列就行&#xff09;如下图&am…

二叉查找树、二叉搜索树、二叉排序树算法分析及实现

一、几个概念 二叉树&#xff08;Binary Tree&#xff09;&#xff0c;是 n&#xff08;n > 0&#xff09;个结点&#xff08;每个结点最多只有2棵子树&#xff09;的有限集合&#xff0c;该集合或为空集&#xff0c;称为空二叉树&#xff0c;或由一个根节点和两颗互不相交…

算法练习第16天|101. 对称二叉树

101. 对称二叉树 力扣链接https://leetcode.cn/problems/symmetric-tree/description/ 题目描述&#xff1a; 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 示例 1&#xff1a; 输入&#xff1a;root [1,2,2,3,4,4,3] 输出&#xff1a;true示例 2&#x…

稀碎从零算法笔记Day46-LeetCode:互质树

这几天有点懈怠了 题型&#xff1a;树、DFS、BSF、数学 链接&#xff1a;1766. 互质树 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;LeetCode 题目描述 给你一个 n 个节点的树&#xff08;也就是一个无环连通无向图&#xff09;&#xff0c;节点编号从 0 到 …

力扣207.课程表

你这个学期必须选修 numCourses 门课程&#xff0c;记为 0 到 numCourses - 1 。 在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出&#xff0c;其中 prerequisites[i] [ai, bi] &#xff0c;表示如果要学习课程 ai 则 必须 先学习课程 bi 。 例如…

2024-3-29 群讨论:如何看到一个线程的所有 JFR 事件

以下来自本人拉的一个关于 Java 技术的讨论群。关注公众号&#xff1a;hashcon&#xff0c;私信拉你 如何查看一个线程所有相关的 JFR 事件 一般接口响应慢&#xff0c;通过日志可以知道是哪个线程&#xff0c;但是如何查看这个线程的所有相关的 JFR 事件呢&#xff1f;JMC 有…

77、WAF攻防——权限控制代码免杀异或运算变量覆盖混淆加密传参

文章目录 WAF规则webshell免杀变异 WAF规则 函数匹配 工具指纹 webshell免杀变异 php 传参带入 eval可以用assert来替换,assert也可以将字符串当作php代码执行漏洞 php 变量覆盖 php 加密 使用加密算法对php后门进行加密 php 异或运算 简化:无字符webshellP 无数字字母rc…

WPS二次开发系列:Gradle版本、AGP插件与Java版本的对应关系

背景 最近有体验SDK的同学反馈接入SDK出现报错&#xff0c;最终定位到原因为接入的宿主app项目的gradle版本过低导致&#xff0c;SDK兼容支持了android11的特性&#xff0c;需要对应的gradle插件为支持android11的版本。 现象 解决方案 将gradle版本升级至支持android11的插件版…