使用阈值图修改角色脸部阴影

news2024/11/17 8:29:19

大家好,我是阿赵。
之前介绍了通过修改模型顶点法线的方法,来解决角色模型脸部光影问题。这里再顺便介绍一下第二种方法,使用阈值图修改角色脸部阴影

一、角色脸部光影的问题

在这里插入图片描述
在这里插入图片描述

这个问题之前讨论过,由于角色脸部法线复杂,如果通过点乘顶点法线和光照方向来算阴影,会导致某些局部位置的影子很奇怪。
之前说过,解决这个问题,我有2个手段,第一个手段修改脸部法线映射,已经在之前的文章里面介绍过了。这里再说说第二个手段,通过阈值图来控制角色脸部法线。
通过阈值图控制角色脸部阴影,比较有名的是游戏《原神》。
这个方法相对于修改法线,在实现上会复杂很多,因为他涉及到一些比较复杂的图形计算原理。比如要生成阈值图,就要先知道SDF是什么,怎样去获取每张图片的的SDF,然后怎样通过插值的方式,从多张SDF数据合并出一张能用的阈值图。
这个问题估计有很多人会看不懂,所以先说说结论吧,起码知道我们现在要做的是一件怎样的事情。
1.绘制多张阴影范围的贴图,用于表达在不同的光线角度下,阴影的分布情况。由于这里是一个做得比较快的范例,所以我是直接随便在PS里面用钢笔勾出来的,所以边缘有点硬,明白意思就行。
在这里插入图片描述

2.通过上面的多张图,计算出SDF,并合并成一张阈值图
在这里插入图片描述

3.编写一个shader,可以通过光线方向来控制阈值,显示阴影在模型上面
在这里插入图片描述
在这里插入图片描述

这里由于画得不仔细,眼睛的部分并没有画好,所以眼睛部分的阴影计算是错误的
4.混合模型原有的颜色,得出正确的结果

在这里插入图片描述

二、SDF的介绍

1、SDF是什么
有符号距离场(SignedDistanceField,SDF)。听名字给人感觉就是一个比较高深的东西。拆分一下,可以理解成:
1.有符号
这个东西是有正负数的,正数和负数分别代表了不同的含义
2.距离
这个东西的数据,主要是为了表达距离关系的
3.场
叫得上场的,肯定就不是单个点能形成的,它是很多点的数据集合,每个点记录的数据是这个整体的场里面的一个相对关系。
具体SDF是什么含义,它有什么用途,各位可以轻易的百度得到,如果不想百度,我也可以简单归纳一下。
所谓的SDF,是记录了一张图片上面,每一个像素点,离图案边缘的距离。比如我们有一个圆的图片,圆的边缘上的像素点,我们记录为0,在圆外面的像素点,我们记录为正数,离边缘越远的点值越大。在圆内部的像素点,我们记录为负数,离边缘越远的点值越小。所以远离边缘的地方,用颜色表达就是越来越白,边缘是灰色,在内部离边缘越远,就越黑
在这里插入图片描述

2、怎样去计算SDF
从上面的分析去看,有一个很重要的问题我们需要解决的,就是怎样求所谓的边缘?
如果是彩色图片,那么方法很明显是采样一个顶点周围的一圈点,然后通过偏导数ddx/ddy来求出他是否处于颜色变化很大的边缘。
但如果只是求出边缘,对于SDF来说还不够。因为SDF的另外一个重点是,它有符号,它要知道当前的点是属于图形的内部还是外部。这个问题如果在彩色图里面,就比较难以定义了。
幸好,我们需要求的SDF,是有明确的内外之分的图形,比如上面我画的阴影遮罩图,非黑即白。我们可以很简单的通过自己的规则,去指定图形的内外,比如黑色的是图形外,白色是图形内,之类。你也可以根据自己的需要反过来定义也可以。
接下来的事情,就是逐张图片的逐个像素去遍历,然后求出它是否边缘,如果不是边缘,求出它离自己最近的边缘的距离,然后根据黑白色,给它赋予正负的符号。
这里有个小优化,求离自己最近的边缘,并不是无限的遍历,而是可以设置一个最大值,如果在一定范围内都找不到边缘,就给他一个极限值1或者-1。
于是,通过上面绘制的阴影图,就生成了下面的这些SDF图。
在这里插入图片描述

其中纯黑是在内部纯白是在外部,边缘是灰色。越黑或者越白就是离边缘越远。
下面这段C#代码是在Unity3D里面,通过Texture2D生成SDF数据的一个类:

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

public class SDFTexData
{
    public string texName = "";
    public int width = 0;
    public int height = 0;
    public Dictionary<int, Dictionary<int, float>> pixels;
    public Dictionary<int, Dictionary<int, float>> sdfs;
    public int checkRange = 10;
    private float edgeVal = 0.1f;

    public void SetData(Texture2D tex)
    {
        width = tex.width;
        height = tex.height;
        checkRange = Mathf.FloorToInt((float)width / 10);
        pixels = new Dictionary<int, Dictionary<int, float>>();
        sdfs = new Dictionary<int, Dictionary<int, float>>();
        for(int j = 0;j<height;j++)
        {
            for(int i = 0;i<width;i++)
            {
                Color col = tex.GetPixel(i, j);
                AddPixel(i, j, col.r);
            }
        }
        CreateSDF();
        CreateTestTex();
    }
    private void CreateTestTex()
    {
        Texture2D tex = new Texture2D(width, height);
        for(int j = 0;j<height;j++)
        {
            for(int i = 0;i<width;i++)
            {
                float sdf = GetSDF(i, j);
                float val = (sdf + 1) / 2;
                Color col = new Color(val, val, val, 1);
                tex.SetPixel(i, j, col);
            }
        }
        tex.Apply();
        byte[] bs = tex.EncodeToJPG();
        File.WriteAllBytes("g:/sdf/" + texName + ".jpg", bs);
    }
    private void CreateSDF()
    {
        for(int j = 0;j<height;j++)
        {
            for(int i = 0;i<width;i++)
            {
                float val = GetSDFVal(i, j);
                AddSDF(i, j, val);
            }
        }
    }

    private float GetSDFVal(int x,int y)
    {
        float tx = -9999;
        float ty = -9999;
        float val = 9999;
        for(int i = 1;i<checkRange+1;i++)
        {
            val = GetOneRangeSDF(x, y, i);
            if(val<999)
            {
                return val;
            }
        }
        float selfVal = GetPixel(x, y);
        if(selfVal > edgeVal)
        {
            return -1;
        }
        else
        {
            return 1;
        }
        
    }
    private bool IsOutOfTex(int x,int y)
    {
        if(x<0||x>=width||y<0||y>height)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    private float GetOneRangeSDF(int x,int y,int range)
    {
        float val = 9999;
        float tempVal = 9999;
        float selfVal = GetPixel(x, y);
        float tempCol = -9999;
        if(selfVal> edgeVal)//在内部或者边缘
        {
            for (int i = -range; i < range + 1; i++)
            {
                for (int j = -range; j < range + 1; j++)
                {
                    int ox = x + i;
                    int oy = y + j;
                    if(IsOutOfTex(ox,oy))
                    {
                        continue;
                    }
                    if (ox==x && oy==y)
                    {
                        continue;
                    }
                    tempCol = GetPixel(ox, oy);
                    if(tempCol <= edgeVal)
                    {
                        if(Mathf.Abs(i-x)<=1&&Mathf.Abs(j-y)<=1)//下一格就是外部
                        {
                            val = 0;
                            return val;
                        }
                        else
                        {
                            tempVal = Mathf.Sqrt(Mathf.Pow((Mathf.Abs(ox - x) ), 2) + Mathf.Pow((Mathf.Abs(oy - y)), 2));
                            if (tempVal<val)
                            {
                                val = tempVal;
                            }
                        }
                    }
                }
                if(val>0&&val<100)
                {
                    break;
                }
            }
            if(val >100)
            {
                return val;
            }
            else
            {
                float result = val / checkRange;
                if (result > 1)
                {
                    result = 1;
                }
                return result*-1;
            }

        }
        else//在外部
        {
            for (int i = -range; i < range + 1; i++)
            {
                for (int j = -range; j < range + 1; j++)
                {
                    int ox = x + i;
                    int oy = y + j;
                    if (IsOutOfTex(ox, oy))
                    {
                        continue;
                    }
                    if (ox == x && oy == y)
                    {
                        continue;
                    }
                    tempCol = GetPixel(ox, oy);
                    if (tempCol > edgeVal)
                    {
                        tempVal = Mathf.Sqrt(Mathf.Pow((Mathf.Abs(ox - x)),2) + Mathf.Pow((Mathf.Abs(oy - y)), 2));
                        if (tempVal < val)
                        {
                            val = tempVal;
                        }                        
                    }
                }
            }
            if (val > 100)
            {
                return val;
            }
            else
            {
                float result = val / checkRange;
                if(result >1)
                {
                    result = 1;
                }
                return result;
            }
        }

    }

    public void AddPixel(int x,int y,float val)
    {
        if(pixels == null)
        {
            pixels = new Dictionary<int, Dictionary<int, float>>();
        }
        if(pixels.ContainsKey(x)==false)
        {
            pixels.Add(x, new Dictionary<int, float>());
        }
        if(pixels[x].ContainsKey(y))
        {
            pixels[x][y] = val;
        }
        else
        {
            pixels[x].Add(y, val);
        }
    }

    public float GetPixel(int x,int y)
    {
        if(pixels==null||pixels.ContainsKey(x)==false||pixels[x].ContainsKey(y)==false)
        {
            return 0;
        }
        return pixels[x][y];
    }

    public void AddSDF(int x, int y, float val)
    {
        if (sdfs == null)
        {
            sdfs = new Dictionary<int, Dictionary<int, float>>();
        }
        if (sdfs.ContainsKey(x) == false)
        {
            sdfs.Add(x, new Dictionary<int, float>());
        }
        if (sdfs[x].ContainsKey(y))
        {
            sdfs[x][y] = val;
        }
        else
        {
            sdfs[x].Add(y, val);
        }
    }

    public float GetSDF(int x, int y)
    {
        if (sdfs == null || sdfs.ContainsKey(x) == false || sdfs[x].ContainsKey(y) == false)
        {
            return -9999999;
        }
        return sdfs[x][y];
    }
}

这是我比较习惯的面向对象的写法,创建一个SDFTexData对象,然后通过SetData方法传入一张Texture2D,遍历所有像素点,生成SDF数据,最后调用CreateTestTex方法,保存一张图片。由于是测试用的方法,所以我临时保存在了g盘的sdf文件夹,各位可以根据自己情况修改保存的方法。

三、阈值图的原理和生成

上面介绍了很久,也生成了一堆SDF图,那究竟为什么生成阈值图需要先生成SDF呢?
这里先来看看阈值图究竟是什么含义的一张图片。
在这里插入图片描述

生成后的阈值图大概是长这样的。它的含义是,当我们输入某个值的时候,大于这个值的像素点都能显示,小于它的像素点都不显示。所以我们输入的这个值,其实就是代表了在一个线性过渡过程中,能让某个点显示的开关值。
刚才我们只画了7张阴影图,是代表了输入一个0到1的范围时阴影的情况,当输入的值是0的时候,显示第一张图的阴影,当输入1的时候应该显示第7张图的阴影。 那么问题来了,假如我们随意输入0-1里面任意的一个值时,该怎样去决定用哪张图作为阴影?或者哪张图都不对,应该怎样从2张图之间取一个过渡值呢?
这时候,SDF的优势就体现出来了,比如说,某个像素点在0.5的时候离边缘的值是0.2,到了0.7的时候它离边缘的值就变成了-0.4,那么通过插值就可以知道,它在0.6的时候值是-0.1了。
所以通过这7张SDF图,我们可以求出任意一个时间点某个像素离边缘的距离。由于我们只要求出像素点在边缘,或者在内部时的开始时间点就够了,就能知道某个像素点在0到1的过程中,究竟在哪个时间应该显示出来了。
剩下的工作就很简单了,每个像素点遍历7张图,算出它到达边缘的实际时间点,然后保存下来就可以了。于是就生成了上面的那张阈值图了。
下面提供一下生成阈值图的代码:

    private void CreateThresholdTex(List<SDFTexData> texList)
    {
        width = texList[0].width;
        height = texList[0].height;
        saveTex = new Texture2D(width, height);
        spaceVal = 1 / (float)(texList.Count - 1);
        float col1 = -99999;
        float col2 = -99999;
        for (int j = 0; j < height; j++)
        {
            for (int i = 0; i < width; i++)
            {
                float curval = 1;
                col1 = -99999;
                col2 = -99999;
                for (int k = 0; k < texList.Count; k++)
                {
                    if (col1 < -999)
                    {
                        col1 = texList[k].GetSDF(i, j);
                        if (col1 <= 0)
                        {
                            curval = k * spaceVal;
                        }
                    }
                    else
                    {
                        col2 = texList[k].GetSDF(i, j);
                        if (col2 <= 0)
                        {
                            curval = spaceVal * (k - 1) + spaceVal * (col1) / (col1 - col2);
                        }
                        else
                        {
                            col1 = col2;
                        }
                    }
                    if (curval < 1)
                    {
                        break;
                    }
                }
                curval = 1 - curval;
                saveTex.SetPixel(i, j, new Color(curval, curval, curval, 1));
            }
        }
        saveTex.Apply();
        byte[] saveBs = saveTex.EncodeToJPG();
        File.WriteAllBytes("g:/sdf/yuzhi.jpg", saveBs);
}

通过传入SDF数据的数组,然后生成阈值图。最后由于是测试,我也是随便保存在了g盘的sdf文件夹,各位可以看情况修改。

四、编写shader实现

有了阈值图,我们就可以写Shader来给模型设定阴影了。这里要注意几个问题:
1、阈值图由于是左右变化的,所以它不可能产生上下的阴影变化,所以在计算光照的时候,应该只取x和z方向
float2 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)).xz;
2、我们计算的阈值图,是只有一个方向的,比如光线从左边转到正面的一个过程,缺少了光线从右边到正面的过程,所以我们应该采样2次,其中一个过程采样阈值的UV要把x坐标乘以-1,让图片翻转。然后要定义一个相对于角色在左方向或者右方向的向量,通过这个向量和光线角度做夹角,来判断光线现在是在模型的左边照射还是右边照射,来控制2次采样的图片的显示。
3、实际上光照有前后之分,当光线在背后的时候,应该不需要计算阈值,直接把脸给全部附上阴影。所以我们要定一个相对于角色的前方向向量,通过这个向量和光线角度做夹角,来判断光线现在在角色前面还是后面。
接下来是具体的shader代码:

Shader "Unlit/ToonFaceThreshold"
{
    Properties
    {
		_MainTex("MainTex", 2D) = "white" {}
		_shadowLength("shadowLength", Range(0 , 1)) = 0
		_shadowMap("shadowMap", 2D) = "white" {}
		_min("min", Range(0 , 1)) = 0.24
		_max("max", Range(0 , 1)) = 0.28
		_shadowCol("shadowCol", Color) = (0,0,0,0)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
			
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {                
                float4 vertex : SV_POSITION;				
				float2 uv : TEXCOORD0;
				float2 shadowUV : TEXCOORD1;
				float4 worldPos:TEXCOORD12;
            };

			uniform sampler2D _MainTex;
			uniform float4 _MainTex_ST;
			uniform float _shadowLength;
			uniform float _min;
			uniform float _max;
			uniform sampler2D _shadowMap;
			uniform float4 _shadowMap_ST;
			uniform float4 _shadowCol;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				o.shadowUV = TRANSFORM_TEX(v.uv, _shadowMap);
                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
				float3 transPos = mul(unity_ObjectToWorld, float4(float3(0,0,0), 1)).xyz;
				float2 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)).xz;
				float3 frontOffset = mul(unity_ObjectToWorld, float3(0, 0, 1));
				float2 frontPos = (frontOffset + transPos).xz;
				float frontVal = dot(frontPos, lightDir);
				float frontDotVal = step(frontVal, -0.5);

				float3 rightOffset = mul(unity_ObjectToWorld,float3(-1, 0, 0));
				float2 rightPos = (rightOffset + transPos).xz;
				float rightVal = dot(rightPos, lightDir);

				

                half4 baseCol = tex2D(_MainTex, i.uv);
				half4 shadowCol = tex2D(_shadowMap, i.shadowUV);

				float frontShadowVal = shadowCol.r - frontVal;
				float frontClamp = clamp(frontShadowVal, 0, 1);
				float frontSmoothVal =  smoothstep(_min, _max, frontClamp);

				float2 rightShadowUV = float2(i.shadowUV.x*-1, i.shadowUV.y);
				half4 shadowRightCol = tex2D(_shadowMap, rightShadowUV);
				float rightShadowVal = shadowRightCol.r - frontVal;
				float rightClamp = clamp(rightShadowVal, 0, 1);
				float rightSmoothVal = smoothstep(_min, _max, rightClamp);

				float rightStepVal = step(rightVal, 0);

				float frontMulVal = frontSmoothVal * rightStepVal;
				float rightMulVal = rightSmoothVal * (1 - rightStepVal);
				float finalDirVal = clamp((frontMulVal + rightMulVal), 0, 1);

				float finalShadowVal = frontDotVal*_shadowLength + (1 - frontDotVal)*(_shadowLength*finalDirVal*(1 - frontDotVal));
				float4 finalCol = baseCol * (1 - finalShadowVal) + (baseCol*_shadowCol*finalShadowVal);

                return finalCol;
            }
            ENDCG
        }
    }
}

在这里插入图片描述
在这里插入图片描述

最后,得到了这样的结果。可以看出来,得到的影子就和阈值图一样,没有因为法线错乱而导致影子出错的情况。

五、总结

上面详细介绍了怎样通过阈值图来控制模型的阴影。对比使用法线映射的解决办法,它有一些优点:
1、可以通过美术绘制,得到很精确的阴影效果,包括了眼睛鼻子在过渡时应该出现怎样的阴影变化,都可以绘制出来。如果觉得7张图不够,做多几张也可以的。从可控性来说,这种方法应该是比较好的。
2、可以脱离光照,单独的通过某个变量值来控制阈值的变化,达到改变阴影的效果。
它的缺点同样也很明显
1、它只能生成左右的阴影,没有办法同时处理上下方向的阴影。这可以说是硬伤。
2、生成SDF图和阈值图需要比较长的时间。我在例子里面,使用的都是256256的图片,生成7张图加阈值图,也需要将近20分钟。但其实256的精度是不够的,所以我又把阈值图放到ps里面修改成512512,然后加了模糊处理,这样才勉强能看。如果要生成1024级别的图片,就更是慢到我自己都不敢尝试了。或者是我自己的算法不够优化吧,各位可以多尝试一下。
3、由于需要计算各种夹角来判断灯光位置和阈值的关系,所以shader的计算量会比单纯的顶点法线和光线角度求点乘的传统阴影计算方式复杂度要高。看我上面那个shader好像几十行就写完,其实里面的计算我个人感觉也挺复杂的。

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

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

相关文章

2023年最新交通航线(飞机、高铁)信息数据合集(含经纬度匹配)

中国高铁航线数据库Chinese High-speed Rail and Airline Database&#xff0c;CRAD&#xff09;是一个专门收集和管理航空公司和高铁公司交通航线信息的数据仓库。它包含了航线的起始点、终止点、中转点、飞行时间、票价、座位数、乘客数量、货物数量等信息。 该数据仓库可以运…

Fortinet 发布《2022下半年度全球威胁态势研究报告》,七大发现值得关注

全球网络与安全融合领域领导者Fortinet&#xff08;NASDAQ&#xff1a;FTNT&#xff09;&#xff0c;近日发布《2022 下半年度全球威胁态势研究报告》。报告指出&#xff0c;相对于组织攻击面的不断扩大以及全球威胁态势的持续演进&#xff0c;网络犯罪分子设计、优化技术与战术…

设计模式---单例模式

目录 1 简介 2 实现 3 单例模式的几种实习方式 1. 饿汉式 2. 懒汉式&#xff0c;线程不安全 3. 懒汉式&#xff0c;线程安全 4. 双检锁/双重校验锁(DCL, double-check locking) 5. 登记式/静态内部类 4 单例模式的优缺点 1 简介 单例模式(Singleton Pattern) 是 Java…

React 中五种常见的样式

React 中五种常见的样式策略 React中的样式策略主要有以下几种&#xff1a; 内联样式&#xff1a; 内联样式就是在JSX元素中&#xff0c;直接定义行内的样式&#xff1b;CSS样式表&#xff1a; 这也是我们最常用的样式策略&#xff0c;使用单独的样式表&#xff0c;使用CSS或…

lvgl 笔记 按钮部件 (lv_btn) 和 开关部件 (lv_switch)

按钮基础使用方法&#xff1a; lv_btn 和 lb_obj 使用方法一样&#xff0c;只是外表并不相同&#xff0c;基础创建方法只需一行代码。 lv_obj_t* btn lv_btn_create(lv_scr_act()); 添加大小和位置&#xff1a; lv_obj_t* btn lv_btn_create(lv_scr_act()); lv_obj_set_s…

一个小故障:vTaskGenericNotifyGiveFromISR卡死的解决

平台&#xff1a;gd32f103 freertos V10.4.3 LTS Patch 2 调试的时候发现一个问题&#xff1a; 在中断中使用 vTaskNotifyGiveFromISR(TaskHandle_ToCpu_IIC,NULL); //唤醒任务 但是程序却出现卡死现象&#xff1a; 在vTaskGenericNotifyGiveFromISR函数中。 用调试器看到…

C++ STL:string类的概述及常用接口说明

目录 一. 什么是STL 二. string类的概述 三. string类的常用接口说明 3.1 字符串对象创建相关接口&#xff08;构造函数&#xff09; 3.2 字符串长度和容量相关接口 3.3 字符访问相关接口函数 3.4 字符串删改相关接口函数 3.5 字符查找和子串相关接口函数 3.6 迭代器相…

c++11右值引发的概念

右值引用右值&&左值c11增加了一个新的类型&#xff0c;右值引用&#xff0c;记作&#xff1a;&&左值是指在内存中有明确的地址&#xff0c;我们可以找到这块地址的数据&#xff08;可取地址&#xff09;右值是只提供数据&#xff0c;无法找到地址&#xff08;不…

跨时钟域CDC

https://www.cnblogs.com/icparadigm/p/12794483.html https://www.cnblogs.com/icparadigm/p/12794422.html 亚稳态 是什么 时序逻辑在跳变时&#xff0c;由于异步信号、跨时钟域等原因&#xff0c;不满足setup或hold条件&#xff0c;输出在0和1之间产生振荡。 原因 D触发…

Canny算法原理和应用

Canny算法的原理使用高斯滤波器滤波使用 Sobel 滤波器滤波获得在 x 和 y 方向上的输出&#xff0c;在此基础上求出梯度的强度和梯度的角度edge为边缘强度&#xff0c;tan为梯度方向上图表示的是中心点的梯度向量、方位角以及边缘方向&#xff08;任一点的边缘与梯度向量正交&am…

如何在MySQL 8中实现数据迁移?这里有一个简单易用的方案

文章目录前言一. 致敬IT领域的那些女性二. 进制方式安装MySQL2.1 下载软件包2.2 配置环境&#xff1a;2.2.1 配置yum环境2.2.2 配置安全前的系统环境2.3 开始安装2.4 初始化MySQL2.5 修改配置文件2.6 将MySQL设为服务并启动测试三. MySQL数据迁移总结前言 正好赶上IT女神节&am…

《Linux运维实战:ansible中的变量定义及以及变量的优先级》

一、配置文件优先级 Ansible配置以ini格式存储配置数据&#xff0c;在Ansible中⼏乎所有配置都可以通过Ansible的Playbook或环境变量来重新赋值。在运⾏Ansible命令时&#xff0c;命令将会按照以下顺序查找配置⽂件。 # ⾸先&#xff0c;Ansible命令会检查环境变量&#xff0c…

【node : 无法将“node”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。 最全面有效的解决方案】

执行nodejs文件错误&#xff1a; 这个错误提示通常是由于你的系统无法识别 "node" 命令&#xff0c;可能是由于你没有正确地安装或配置 Node.js 环境变量。 问题描述 ​​​​​​​​​​​​​​ 原因分析&#xff1a; 可能原因包括&#xff1a; 1.Node.js未正确安…

JVM堆与堆调优以及出现OOM如何排查

调优的位置——堆 Heap&#xff0c;一个JVM只有一个堆内存&#xff0c;堆内存的大小是可以调节的。 类加载器读取了类文件后&#xff0c;一般会把什么东西放到堆中?类&#xff0c;方法&#xff0c;常量&#xff0c;变量~&#xff0c;保存我们所有引用类型的真实对象; 堆内存中…

【Linux修炼】15.进程间通信

每一个不曾起舞的日子&#xff0c;都是对生命的辜负。 进程间通信进程间通信一.理解进程间通信1.1 什么是通信1.2 为什么要有通信1.3 如何进行进程间通信二.管道2.1 匿名管道2.2 匿名管道编码部分2.3 管道的特点2.4 如何理解命令行中的管道2.5 进程控制多个子进程三.命名管道3.…

openEuler用户软件仓(EUR)介绍

什么是 EUR EUR(openEuler User Repo)是openEuler社区针对开发者推出的个人软件包托管平台&#xff0c;目的在于为开发者提供一个易用的软件包分发平台。 链接&#xff1a;https://eur.openeuler.openatom.cn/ 为什么我们需要 EUR 在操作系统的世界&#xff0c;软件包是一等…

数据库基本功之复杂查询-多表连接

1. 简单查询的解析方法 全表扫描:指针从第一条记录开始,依次逐行处理,直到最后一条记录结束;横向选择纵向投影结果集 2. 多表连接 交叉连接(笛卡尔积) 非等值连接 等值连接 内连 外连接(内连的扩展,左外,右外,全连接) 自连接 自然连接(内连,隐含连接条件,自动匹配连接字段) …

以创作之名致敬女性开发者

作者简介&#xff1a;一名云计算网络运维人员、每天分享网络与运维的技术与干货。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​​ 前言 在昨天的2023年3月8日&#xff0c;是咱们女性朋友的节日妇女节&#xff0c;本章将会…

腾讯云GPU游戏服务器/云主机租用配置价格表

用于游戏业务的服务器和普通云服务器和主机空间是不同的&#xff0c;游戏服务器对于硬件的配置、网络带宽有更大的要求&#xff0c;一般游戏服务器根据不同的配置和适用场景会有十几元一小时到几十元一小时&#xff0c;而且可以根据不同的按量计费。而普通的云服务器可能需要几…

Linux程序替换

Linux程序替换创建子进程的目的&#xff1f;程序替换如何实现程序替换&#xff1f;什么是程序替换&#xff1f;先见一见单进程版本的程序替换程序替换原理多进程版本的程序替换execl函数组简易版Shell创建子进程的目的&#xff1f; 目的:为了帮助父进程完成一些特定的任务&…