Cocos Creator 3.8 后期效果 Shader 编写(2/2) 进阶篇

前言

在上一篇文章中,麒麟子给大家分享了如何在 Cocos Creator 3.8 中的自定义管线中,添加属于自己的后期效果 Shader。 但基于 BlitScreen 的方案,我们只能编写最简单后效 Shader,如果我们想要支持更多复杂的 Shader,比如模糊、景深等等效果,就需要配合代码才能实现。

今天麒麟子就用高斯模糊来演示如何编写一个多 Pass 的后效 Shader。

准备 Shader

高斯模糊的涉及到的内容比较多,什么正态分布、高斯函数、高斯卷积核。

如果对数学没兴趣,上面这些统统不用管。

简单来说,高斯模糊就是把图片上的每一个像素都用下面的流程处理一遍。

直白来说,就是一个简单的加权求和:采样目标像素的同时,再采样一些周围的像素;并且每一个像素给一个权重(权重和为1.0)。最终像素值 = 所有(像素 x 权重)的和。

如果想要效果好,就多迭代几次,迭代次数越多,画面越好,但性能开销越高。

我们创建一个 Cocos Shader 文件,命名为 “gaussian-blur.effect” 然后编写下面的内容。

CCEffect %{
  techniques:
  - passes:
    - vert: blur-hor-vs
      frag: blur-fs
      pass: blur-x
      depthStencilState:
        depthTest: false
        depthWrite: false
    - vert: blur-vert-vs
      frag: blur-fs
      pass: blur-y
      depthStencilState:
        depthTest: false
        depthWrite: false
}%

CCProgram blur-hor-vs %{
  //...
}%
CCProgram blur-hor-vs %{
  //...
}%

CCProgram blur-fs %{
  //...
}%

//为了方便文章阅读
//完整 Shader 被放到了文末

可以看到,整个 Cocos Shader 只有两个 Pass,一个用来水平模糊,一个用来竖直模糊。

为了不影响阅读,完整 Shader 放在了文末。

注意:Cocos Creator 3.8.0 版本如果新增了后效 Shader,需要重启编辑器才能识别。后面版本中会优化这个流程。

编写属性组件

Cocos Creator 3.8 中,只需要在节点上添加对应的后期效果组件,就可以启动对应的后期效果。

自定义后期管线提供了一个 postProcess.PostProcessingSetting 组件类,我们可以通过继承它类来实现后效参数的可视化界面配置。

通过它自定义出来的后期效果 ,是完全可以达到和内置的后期效果一样的使用体验的。

新建一个 TS 脚本文件,起名为 “GaussianBlur.ts”,然后输入以下代码。

代码片段1:

import { _decorator, gfx, postProcess, Material, EffectAsset, renderer, rendering, Vec4 } from 'cc';
const { Format  } = gfx

const { ccclass, property, menu, executeInEditMode } = _decorator;

@ccclass('GaussianBlur')
@menu('PostProcess/GaussianBlur')
@executeInEditMode
export class GaussianBlur extends postProcess.PostProcessSetting{

    @property(EffectAsset)
    _effectAsset: EffectAsset | undefined

    @property(EffectAsset)
    get effect () {
        return this._effectAsset;
    }
    set effect (v) {
        this._effectAsset = v;
        if(this._effectAsset == null){
            this._material = null;
        }
        else{
            if(this._material == null){
                this._material = new Material();
            }
            this._material.reset({effectAsset:this._effectAsset});
        }
        this.updateMaterial();
    }
    
    @property
    iterations = 3;

    @property
    get blurRadius(){
        return this._blurParams.x;
    }
    set blurRadius(v){
        this._blurParams.x = v;
        this.updateMaterial();
    }


    private _material:Material;
    public get material():Material{
        return this._material;
    }

    @property
    private _blurParams:Vec4 = new Vec4(1.0,0.0,0.0,0.0);
    public get blurParams():Vec4{
        return this._blurParams;
    }

    updateMaterial(){
        if(!this._material){
            return;
        }
        this._material.setProperty('blurParams', this.blurParams);
    }

    protected start(): void {
        if(this._effectAsset){
            this._material = new Material();
            this._material.initialize({effectAsset:this._effectAsset});
            this._material.setProperty('blurParams', this.blurParams);
        }
    }
}

接下来,我们就可以通过“添加组件“按钮,把 PostProcess/GansussianBlur 添加到后处理结点上了。

可以看到,在 Inspector 面板上,它接受 3 个参数。

  • effect:用于指定用于这个后期效果的 Shader 文件
  • iterations:用于指定需要迭代多少次,迭代次数越多越模糊
  • blurRadius:采样临近像素时的偏移,偏移量越大越模糊

这个时候是没有任何效果的,因为还没有实现对应的渲染代码。

编写渲染代码

Cocos Creator 3.8 中的后处理管线是基于新版自定义管线的,而新版的自定义管线是基于 RenderGraph 架构的。

你可以简单地认为,我们想要实现的单个后处理效果,对应的就是渲染流程图上的一个节点。

后处理管线提供了 postProcess.SettingPass 给我们,用来编写自己的后处理渲染效果。(也可以叫,自定义后处理节点的资源和行为)。

接下来,我们来做真正的渲染实现。 我们需要继承自定义管线中的 postProcess.SettingPass 类,并实现并要的代码。

  • get setting:获取配置信息,对应的就是上面实现的界面组件
  • checkEnable:用于判断此后效是否开启
  • name:后效的名字,一般保持和类名一致即可
  • outputNames:最终输出的 RT 数组。(临时用的 RT 不用放在这里)
  • render:用于执行渲染流程

完整编码如下。

代码片段2:

export class GaussianBlurPass extends postProcess.SettingPass {
    get setting () { return this.getSetting(GaussianBlur); }

    checkEnable (camera: renderer.scene.Camera) {
        let enable = super.checkEnable(camera);
        if (postProcess.disablePostProcessForDebugView()) {
            enable = false;
        }
        return enable && this.setting.material != null;
    }

    name = 'GaussianBlurPass';
    outputNames = ['GaussianBlurMap'];

    public render (camera: renderer.scene.Camera, ppl: rendering.Pipeline): void {
        const setting = this.setting;
        if(!setting.material){
            return;
        }

        let passContext = this.context;
        passContext.material = setting.material;

        const cameraID = this.getCameraUniqueID(camera);
        const cameraName = `Camera${cameraID}`;
        const passViewport = passContext.passViewport;

        passContext.clearBlack();
        const format = Format.RGBA8;

        let input = this.lastPass!.slotName(camera, 0);
        for(let i = 0; i < setting.iterations; ++i){
            passContext
                .updatePassViewPort()
                .addRenderPass(`blur-x`, `blur-x${cameraID}`)
                .setPassInput(input, 'outputResultMap')
                .addRasterView('GaussianBlurMap_TMP', format)
                .blitScreen(0)
                .version();

            passContext
                .updatePassViewPort()
                .addRenderPass(`blur-y`, `blur-y${cameraID}`)
                .setPassInput('GaussianBlurMap_TMP', 'outputResultMap')
                .addRasterView(this.slotName(camera), format)
                .blitScreen(1)
                .version();
            input = this.slotName(camera);
        }
    }
}

接下来,我们主要看看 render 处理了哪些事情。

准备工作

每一个 SettingPass 就是一个绘制节点。而节点的数据,我们存在了 context 中。

render 函数会每帧执行,所以需要调用 context.clearBack() 来清理背景。

然后,我们要将材质设置给 context

模糊的处理,需要使用上一个处理流程结束后的画面内容。因此,我们使用 this.lastPass.slotName(camera,0); 来获取。

一切准备就绪后,就进入到了绘制环节。

绘制

这里,我们使用了 iterations 属性来控制总共要迭代的次数。迭代一次,绘制流程就会走一遍。

我们来看看,绘制流程中每一步操作的用途。

  • updatePassViewPort:这个函数用来指定相对分辨率大小,这个根据算法需求来指定就行。如果要保持和后台缓冲区一样大,传入 1.0 即可。

  • addRenderPass:这个函数用来告诉管线,需要执行一次绘制流程。

    • layout:对应的是 Cocos Shader 中的 Pass 名称
    • passName:助记名称,便于调试查看
  • setPassInput:如果有用到自定义管线中的 RT 资源(比如上一次执行的结果),则需要在这里指定,方便自定义管线对资源进行管理。

    • inputName: 自定义管线资源分配的资源名称
    • shaderName: 对应 Cocos Shader 中的 uniform Sampler2D 名称。
  • addRasterView:可以简单理解为,输出结果

    • name:输出的 RT 名称,便于后续流程复用
    • format:输出的 RT 格式,比如,RGBA8888、RGBA16F 等等
  • blitScreen:执行绘制

    • passIdx:Cocos Shader 中的 Pass 索引(这个在后面的版本中会优化一下,到时候,后处理流程可以不用传这个值)。
  • version:无实际意义,可以忽略。

添加到管线

如果只是写好了上面的代码,不进行添加,也是不会生效的。

我们在文件末尾加上下面代码。

代码片段3:

let builder = rendering.getCustomPipeline('Custom') as postProcess.PostProcessBuilder;
if (builder) {
    builder.insertPass(new GaussianBlurPass(),postProcess.BlitScreenPass);
}

首先,我们获取到了 Custom 管线,然后把我们新写的效果添加进去。

回到编辑器中,调节参数,就可以看到我们新写的模糊效果生效咯。

后处理管线源码浅析

为了方便大家理解后处理的渲染流程,麒麟子简单说明一些关键的源码。

代码集

后效相关的类,大多都在 postProcess 下,建议先从 “cc” 引入 postProcess,再使用。

RT 管理

开启后处理管线后,开启了后处理管线效果的摄像机,会自动生成 RT,并设置它的 targetTexture

后效管线基于 RenderGraph 管线架构, Cocos 引擎中使用的 RenderGraph 是基于数据驱动的,会每帧收集需要的 RT 资源,并做统一管理。 因此不需要自己再手工新建 RT。

后效执行顺序

后处理效果的执行,不是按界面添加的顺序来的,而是按照处于数组中的顺序来的。我们通过源码可以看到它的内部顺序如下:

  // pipeline related
  this.addPass(new HBAOPass());
  this.addPass(new ToneMappingPass());

  // user post-processing
  this.addPass(new TAAPass());
  this.addPass(new FxaaPass());
  this.addPass(new ColorGradingPass());
  this.addPass(new BlitScreenPass());
  this.addPass(new BloomPass());

  // final output
  this.addPass(new FSRPass()); // fsr should be final
  this.addPass(forwardFinal);

后处理效果在渲染时,管线会遍历这个数组,依次执行可用的后处理效果。

for (let i = 0; i < passes.length; i++) {
    const pass = passes[i];
    if (!pass.checkEnable(camera)) {
        continue;
    }
    if (i === (passes.length - 1)) {
        passContext.isFinalPass = true;
    }
    pass.lastPass = lastPass;
    pass.render(camera, ppl);
    lastPass = pass;
}

**源码位置:**engine/cocos/rendering/post-process/post-process-builder.ts

后效注意事项

自定义后效添加

在将自己定义的后效 Shader 添加到管线中时,需要注意几个问题:

  1. 必须添加在 ForwardFinal 之前,否则没有效果。所以 builder.addPass 不建议使用
  2. 使用 builder.insterPass 添加新的后效时,如果新后效与旧的后效重名,会先移除旧的后效
  3. builder.insterPass 会将新的后效插入到第二个参数类型指定的后效后面,通常情况下,建议使用 postProcess.BlitScreenPass

效果

  1. TAA+FXAA+FSR 同时开启,才能达到很好的抗锯齿效果,因为 TAA 主要负责动态(帧间)抗锯齿,但会让画面略微变糊,FXAA 主要负责边缘抗锯齿,而 FSR 可以让糊了的画面变清晰。
  2. ColorGrading 是最划算的后期效果,能够快速提升项目画面颜值
  3. 不要过渡依赖 HBAO,因为中低端机跑不动。场景优先使用光照图烘焙出 AO。这样即使HBAO 关闭的情况下,效果依然不会差。
  4. Bloom 的阈值调小,强度调低,可实现全屏泛光,柔和画面。 Bloom 的阈值调大,强度调高,可实现亮部像素曝光效果。

内存

  1. 虽然 RenderGraph会自动管理和复用 RT,但是 后效依然会对内存有所消耗,可自行测试当所需后效开启后,上涨了多少内存。
  2. 内存开销受分辨率决定,请以实机测试为准。
  3. 可以通过 shadingScale 来降低分辨率,从而减少内存和减少渲染开销,提升性能的目的。这个值可以根据当前实际分辨率与设定的机型可用的最大后效分辨率来决定。

性能

  1. 后效会多次读写帧缓存,对 GPU 带宽和像素填充率,纹理填充率要求高。 在中低端机型上很可能造成性能锐减。

  2. 做好高、中、低端机型管理 ,在不同档次的机型上,开启对应的后效组合,确保性能和效果的平衡。

  3. 许多单 Pass 的后效,可以合成一个。 比如 ColorGrading,FXAA,Vignette,FinalPass 可以合成一个。这样可以减少 BlitScreen 次数,提升性能。

  4. HBAO 能够极大地提升空间关系,但也是性能开销大户,谨慎使用。

写在最后

使用后处理效果能极大地提升画面质感,但也需要注意后处理效果带来的额外内存开销和填充率开销。

后效对低端机型很不友好,请做好在低端机上根据性能测试结果做好分档。

另外,后效的种类繁多,且根据具体的项目需求,又可以做特殊优化。因此引擎只能内置常见的后效供大家使用,更多后效果需求,还得开发者们自己来。

希望今天的分享能够对大家有帮助,谢谢大家!

附1:完整源码

新建一个 TS 脚本,将代码片段1代码片段2代码片段3 复制到这个脚本中即可。

附2:完整 Shader

新建一个 Cocos Shader 文件,将下面 Shader 代码复制到文件中即可。

CCEffect %{
  techniques:
  - passes:
    - vert: blur-hor-vs
      frag: blur-fs
      pass: blur-x
      depthStencilState:
        depthTest: false
        depthWrite: false
    - vert: blur-vert-vs
      frag: blur-fs
      pass: blur-y
      depthStencilState:
        depthTest: false
        depthWrite: false
}%

CCProgram blur-hor-vs %{
  precision highp float;
  #include <legacy/input-standard>
  #include <builtin/uniforms/cc-global>
  #include <common/common-define>

  uniform MyConstants {
    vec4 blurParams;
  };

  out vec2 v_uv;
  out vec2 v_uv1;
  out vec2 v_uv2;
  out vec2 v_uv3;
  out vec2 v_uv4;

  void main () {
    StandardVertInput In;
    CCVertInput(In);
    CC_HANDLE_GET_CLIP_FLIP(In.position.xy);
    gl_Position = In.position;
    gl_Position.y = gl_Position.y;
    v_uv = a_texCoord;
    
    vec2 texelSize = cc_nativeSize.zw;
    float blurOffsetX = blurParams.x * texelSize.x;

    v_uv1 = v_uv + vec2(blurOffsetX * 1.0, 0.0);
    v_uv2 = v_uv - vec2(blurOffsetX * 1.0, 0.0);
    v_uv3 = v_uv + vec2(blurOffsetX * 2.0, 0.0);
    v_uv4 = v_uv - vec2(blurOffsetX * 2.0, 0.0);
  }
}%

CCProgram blur-vert-vs %{
  precision highp float;
  #include <legacy/input-standard>
  #include <builtin/uniforms/cc-global>
  #include <common/common-define>

  uniform MyConstants {
    vec4 blurParams;
  };

  out vec2 v_uv;

  out vec2 v_uv1;
  out vec2 v_uv2;
  out vec2 v_uv3;
  out vec2 v_uv4;

  void main () {
    StandardVertInput In;
    CCVertInput(In);
    CC_HANDLE_GET_CLIP_FLIP(In.position.xy);
    gl_Position = In.position;
    gl_Position.y = gl_Position.y;
    v_uv = a_texCoord;
    
    vec2 texelSize = cc_nativeSize.zw;
    float blurOffsetY = blurParams.x * texelSize.y;

    v_uv1 = v_uv + vec2(0.0, blurOffsetY * 1.0);
    v_uv2 = v_uv - vec2(0.0, blurOffsetY * 1.0);
    v_uv3 = v_uv + vec2(0.0, blurOffsetY * 2.0);
    v_uv4 = v_uv - vec2(0.0, blurOffsetY * 2.0);
  }
}%

CCProgram blur-fs %{
  precision highp float;
  #include <builtin/uniforms/cc-global>

  in vec2 v_uv;
  in vec2 v_uv1;
  in vec2 v_uv2;
  in vec2 v_uv3;
  in vec2 v_uv4;

  #pragma rate outputResultMap pass
  uniform sampler2D outputResultMap;

  layout(location = 0) out vec4 fragColor;

  void main () {

    vec3 weights = vec3(0.4026,0.2442,0.0545);
    vec3 sum = texture(outputResultMap, v_uv).rgb * weights.x;

    sum += texture(outputResultMap, v_uv1).rgb * weights.y;
    sum += texture(outputResultMap, v_uv2).rgb * weights.y;
    sum += texture(outputResultMap, v_uv3).rgb * weights.z;
    sum += texture(outputResultMap, v_uv4).rgb * weights.z;

    fragColor = vec4(sum, 1.0);
  }
}%

关于麒麟子

深耕游戏引擎与游戏开发15年,每一滴干货都源自商业项目实践。

用技术资源赋能行业商机,提供实用解决方案、项目分析、技术指导与干货教程!

欢迎私信!

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

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

相关文章

nginx动态同步配置模块nginx-upsync-module

使用场景简介 nginx一般直接在配置文件里配置upstream即可实现负载均衡&#xff0c;但有些特定的环境下此种方式就显得有些局限性。比如后台动态调整节点的时候&#xff1b;调整节点后不想修改配置文件重启nginx。 可以将配置文件从nginx本地迁移到其他第三方服务上如etcd、c…

Claude 2、ChatGPT、Google Bard优劣势比较

​Claude 2&#xff1a; 优势&#xff1a;Claude 2能够一次性处理多达10万个tokens&#xff08;约7.5万个单词&#xff09;。 tokens数量反映了模型可以处理的文本长度和上下文数量。tokens越多&#xff0c;模型理解语义的能力就越强&#xff09;。它在法律、数学和编码等多个…

LinearAlgebraMIT_8_TheRankOfMatrix

这节课中主要讲解根据秩来判断方程组/矩阵的(solvability)解情况&#xff0c;即通过秩来判断(aumented matrix)增广矩阵的解。我们需要直接求解方程组的解就是求解矩阵的解。 x.1 判断(非齐次线性方程组)Axb是否有解 我们以下面这个方程组为例&#xff0c;它具有3个约束条件和…

虹科方案 | 汽车总线协议转换解决方案(二)

上期说到&#xff0c;虹科的PCAN-LIN网关在CAN、LIN总线转换方面有显著的作用&#xff0c;尤其是为BMS电池通信的测试提供了优秀的解决方案。假如您感兴趣&#xff0c;可以点击文末相关链接进行回顾&#xff01; 而今天&#xff0c;虹科将继续给大家带来Router系列在各个领域的…

AI 绘画Stable Diffusion 研究(六)sd提示词插件

大家好&#xff0c;我是风雨无阻。 今天为大家推荐一款可以有效提升我们使用 Stable Diffusion WebUI 效率的插件&#xff0c; 它就是 prompt-all-in-one&#xff0c; 它不但能直接将 WebUI 中的中文提示词转换为英文&#xff0c;还能一键为关键词加权重&#xff0c;更能建立常…

Redis的AOF持久化

除了RDB持久化功能之外&#xff0c;Redis还提供了AOF持久化功能。与RDB 持久化通过保存数据库中的键值对来记录数据库状态不同&#xff0c;AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的&#xff0c;如下图所示。 举个例子&#xff0c;如果我们对空白的数据…

vim学习笔记(致敬vim作者)

vim cheat sheet 30. vim 删除大法 vim 删除某个字符之后改行的其他的字符&#xff1f;删除某行之后的其他行&#xff1f;删除某个字符之后的其他字符&#xff1f;【1】删除单个字符&#xff1f; 跳到要删除的字符位置 按下d键然后按下shift 4键 【2】删除某行之后的其他行…

vite+vue3项目环境搭建

1.安装 npm init vite 2.输入项目名称 vue3-project 3.选择框架 说明&#xff1a;vue 4.选择类别 说明&#xff1a;JavaScript 5.进入文件夹 cd vue3-project yarn npm run dev 6.打开local

数据安全加固:深入解析滴滴ES安全认证技术方案

前文分别介绍了滴滴自研的ES强一致性多活是如何实现的、以及如何提升ES的性能潜力。由于ES具有强大的搜索和分析功能&#xff0c;同时也因其开源和易于使用而成为黑客攻击的目标。近些年&#xff0c;业界ES数据泄露事件频发, 以下是一些比较严重的数据泄露案件&#xff1a; 202…

k8s 自身原理 2

前面我们说到 K8S 的基本原理和涉及的四大组件&#xff0c;分享了前两个组件 etcd 和 ApiServer 这一次我们接着分享一波&#xff1a; 调度器 scheduler控制器管理器 controller manager 调度器 scheduler 调度器&#xff0c;见名知意&#xff0c;用于调度 k8s 资源的&…

石子游戏 dfs + 备忘录 JAVA

Alice 和 Bob 用几堆石子在做游戏。一共有偶数堆石子&#xff0c;排成一行&#xff1b;每堆都有 正 整数颗石子&#xff0c;数目为 piles[i] 。 游戏以谁手中的石子最多来决出胜负。石子的 总数 是 奇数 &#xff0c;所以没有平局。 Alice 和 Bob 轮流进行&#xff0c;Alice 先…

二叉树(4)------收尾

1)最大二叉树 654. 最大二叉树 - 力扣&#xff08;LeetCode&#xff09; 题目解析: 1)首先我们找到了整个数组中最大的元素作为我们的根节点&#xff0c;然后再从左区间中找到最大的元素作为当前根节点的左子树&#xff0c;然后再从右区间里面找到最大的元素作为根节点的右子树…

SpringBoot笔记:SpringBoot 集成 Dataway 多数据源配置(二)

文章目录 前言核心代码和配置yml 配置注入多数据源常用Spi实现swagger 配置自定义 Udf指定数据源进行查询 前言 之前简单介绍了一下 Dataway 使用&#xff0c;本文继续介绍一下它的多数据源配置和使用。 核心代码和配置 yml 配置 # springboot多环境配置 #端口&#xff0c;…

企业服务器器中了360后缀勒索病毒怎么解决,勒索病毒解密数据恢复

随着网络威胁的增加&#xff0c;企业服务器成为黑客攻击的目标之一。近期&#xff0c;上海某知名律师事务所的数据库遭到了360后缀的勒索病毒攻击&#xff0c;导致企业服务器内的数据库被360后缀勒索病毒加密。许多重要的数据被锁定无法正常读取&#xff0c;严重影响了企业的正…

【Mybatis】调试查看执行的 SQL 语句

1. 问题场景&#xff1a; 记录日常开发过程中 Mybatis 调试 SQL 语句&#xff0c;想要查看Mybatis 中执行的 SQL语句&#xff0c;导致定位问题困难 2. 解决方式 双击shift找到mybatis源码中的 MappedStatement的getBoundSql()方法 public BoundSql getBoundSql(Object para…

Stable Diffusion - 人物坐姿 (Sitting) 的提示词组合 与 LoRA 和 Embeddings 配置

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/132201960 拍摄人物坐姿时&#xff0c;需要注意&#xff1a; 选择一个舒适和自然的坐姿&#xff0c;符合个性和心情。可以坐在椅子、沙发、长凳、…

手机app测试

一、安装、卸载、更新、运行 1.安装、卸载 应用是否可以正常安装&#xff08;命令行安装&#xff1b;apk&#xff0f;ipa安装包安装&#xff09;&#xff08;有网&#xff0c;无网是否都正常&#xff09;卸载过程中出现死机&#xff0c;断电&#xff0c;重启等意外的情况&…

Wisej.NET Crack,Wisej.NET的核心功能

Wisej.NET Crack&#xff0c;Wisej.NET的核心功能 Wisej.NET是一个跨平台的web框架&#xff0c;用于使用.NET和C#/VB.NET而不是HTML和JavaScript构建现代HTML5应用程序。它包含创建任务关键型web应用程序所需的一切&#xff0c;包括UI组件、会话处理、状态管理和后端集成。借助…

仿到位|独立版家政上门预约服务小程序家政保洁师傅上门服务小程序上门服务在线派单源码

上门预约服务派单小程序家政 小程序 同城预约 开源代码 独立版. 程序完整,经过安装检测,可放心下载安装。 适合本地的一款上门预约服务小程序,功能丰富,适用多种场景。 程序功能:城市管理/小程序DIY/服务订单/师傅管理/会员卡功能/营销功能/文章功能等等

vmwera中安装的centos8出现ifconfig不可用

刚刚在虚拟机中装好centos结果发现自己的ifconfig命令不可用。 看一下环境变量里有没有ifconfig命令的路径&#xff0c;因为ifconfig是在/sbin路径下的&#xff0c;root用户登录进去才可以运行&#xff0c;先看一下root用户的环境变量。 root用户的环境变量里是有/sbin路径的&a…