1 高度纹理

使用一张纹理改变物体表面法线,为模型提供更多细节。

有两种主要方法:

1、高度映射:使用一张高度纹理(height map)来模拟表面位移(displacement)。得到一个修改后的法线值。

2、法线映射:使用一张法线纹理直接存储表面法线。

1.1 高度纹理

高度图中存储的是强度值(intensity)。越浅越说明向外凸起,越深越向里凹。

缺点是计算复杂,消耗性能。

高度图也会与法线映射一起使用。给出表面凹凸的额外信息。

1.2 法线纹理

法线纹理中存储的是法线方向。法线方向的范围是(1, -1),而像素分量范围是(0,1),通常做如下映射:



1.2.1 模型空间的法线纹理(object-space normal map):

将修改后的模型空间的表面法线存储在一张纹理中。颜色多。

优点:

  • 实现简单,更加直观,计算更少。
  • 纹理坐标的缝合处,缝隙更少。使用一套坐标系,边界处通过插值可以实现平滑变换。

1.2.2 切线空间的法线纹理(tangent-space normal map,常用):

使用模型顶点的切线空间来存储法线。

这个空间原点是顶点本身,z轴是顶点的法线方向,x轴是顶点的切线方向,y轴由法线与切线叉积而成(也叫副切线/副法线,bitangent)。

这种法线实际上记录了各个点在各自的切线空间内的法线扰动方向,颜色多为蓝色(0,0,1)。

这也说明,大多数法线与模型原本法线一致,不需要改变。

优点:

  • 自由度高。模型空间下记录的是绝对法线信息,更换模型会导致完全错误。而切线空间下记录的是相对法线信息,应用到其他网格上也能得到一个合理的效果。
  • 可以进行UV动画。我们可以移动一个纹理的坐标实现一个凹凸移动的效果。这在模型空间下会得到完全错误的结果。这种动画常用于水、火山岩等流体。
  • 可重用。使用一张纹理就可以用到砖头的六个面上。
  • 可压缩。切线空间下z方向总是正方向,我们可以进存储xy方向,从而推导z方向。

1.2.3 Unity中的法线纹理类型

(建议看完下面的代码再来看这一部分)

在Unity中把法线纹理的类型标记为Normal Map可以让Unity在不同平台对纹理进行压缩。

因此在解码的时候要使用UnPackNormal()函数。

当我们把一张高度图导入到Unity后,需要勾选“Create From Grayscale”。这样高度图就可以视作法线纹理了。

Bumpiness用于控制凹凸程度

Filtering决定我们使用哪种方式来计算凹凸程度(Smooth与Sharp)

2 实践

有两种方法:

1、将光照方向、视角方向转换到世界坐标下

2、将采样得到的法线方向变换到世界坐标下

从效率的角度来看,第一种方法更优,因为我们可以在顶点着色器中完成坐标转换,而第二种由于需要对法线纹理进行采样,所以变换过程需要在片元着色器中实现,需要在片元着色器中进行矩阵操作。

从通用性的角度来看,第二种的方法更好。其他操作可能会用到世界坐标中的法线向量,这这时第一种方法还需要把切线空间下法线坐标转换到世界空间下。

2.1 在切线空间下计算光照模型

在顶点着色器中把光照方向、视角方向转换到切线空间下。

在片元着色器中通过纹理采样获得切线空间下的法线坐标。

求模型空间到切线空间下的变换矩阵:

1、先求切线空间到模型空间的变换矩阵(按切线x、副切线y、法线z的顺序按列排列)

2、变换中如果仅存在平移和旋转变换,那么变换的矩阵就等于其转置矩阵。

3、因此模型空间到切线空间下的变换矩阵就是切线空间到模型空间的变换矩阵的转置(按切线x、副切线y、法线z的顺序按行排列)

定义属性如下:

Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_BumpMap ("Normal Map", 2D) = "bump" {}//法线纹理,默认值为自带的bump
_BumpScale ("Bump Scale", Float) = 1.0//凹凸度,为0时意味着法线贴图对原法线无影响
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}

SubShader中的Pass开头定义Tags,定义变量

            Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;//偏移/缩放值
sampler2D _BumpMap;
float4 _BumpMap_ST;//偏移/缩放值
float _BumpScale;
fixed4 _Specular;
float _Gloss;

输入与输出结构体

            struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;//TANGENT是表示切线方向的语义,float4类型,因为需要使用tangent.w分量来计算副切线的方向
float4 texcoord : TEXCOORD0;
}; struct v2f
{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;//定义为float4类型,xy分量存储_MainTex的纹理坐标,zw分量存储_BumpMap的纹理坐标
float3 lightDir : TEXCOORD1;
float3 viewDir : TEXCOORD2;
};

顶点着色器中,先变换pos坐标。

v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);

随后进行纹理坐标变换。

1、将顶点坐标做_MainTex纹理的平移、缩放变换后,将坐标存储至o.uv.xy中。

2、将顶点坐标做_BumpMap纹理的平移、缩放变换后,将坐标存储至o.uv.zw中。

o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;

将法线、切线坐标归一化后叉乘,再乘以v.tangent.w保证方向的正确。(切线有两个方向,因此如果不乘的话,计算出来的副法线可能有两种情况)

cross()

叉乘两个矩阵

然后定义rotation矩阵(按行排列切线、副法线、法线)。也可以使用内置宏:TANGENT_SPACE_ROTATION获得rotation。

使用计算出来的变换矩阵rotation变换光线方向、视角方向。

ObjSpaceLightDir(v.vertex)

输入顶点位置,返回模型空间中该点到光源的光照方向。未被归一化。

代码如下:

//计算副切线(也叫副法线)
float3 binormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
//也可以使用内置宏TANGENT_SPACE_ROTATION获得rotation。
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex));
o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex));
return o;
}

片元着色器部分:

首先将lightDir与viewDir归一化。

            fixed4 frag(v2f i) : SV_Target
{
//归一化
fixed3 tangentLightDir = normalize(i.lightDir);
fixed3 tangentViewDir = normalize(i.viewDir);

随后进行纹理采样,将i.uv.zw中所存储的坐标对应的纹理存储到packedNormal变量中。

tex2D(sampler2D tex, float2 s)函数:

这是CG程序中用来在一张贴图中对一个点进行采样的方法,返回一个float4。

这时,packedNormal变量中所存储的还不是法线向量,而是一个代表法线向量的像素值。我们需要将其映射成法线。

有两种方法。

1、将Unity中把该法线纹理的类型标记为"Normal Map":

必须使用内置函数UnpackNormal(packedNormal);完成映射工作。(不使用内置函数可能会得到错误结果)

2、未将法线纹理标记:

只能自己手动实现映射tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;

代码如下:

                //Get the texel in the normal Map
fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
//采样得到切线空间下的法线方向(把法线纹理中的像素值映射称法线向量,再乘_BumpScale控制凹凸程度)
fixed3 tangentNormal; //If the texture is not marked as "Normal Map"
//tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
//tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy))); //mark the texture as "Normal map", and use build-in function
tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));

随后计算漫反射项与高光反射项:

                //计算漫反射项
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
//计算高光反射项
fixed3 halfDir = normalize(tangentViewDir + tangentLightDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss); return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG

最终代码如下;

Shader "Unity Shaders Book/Chapter 7/NormalMapTangentSpace"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_BumpMap ("Normal Map", 2D) = "bump" {}//法线纹理,默认值为自带的bump
_BumpScale ("Bump Scale", Float) = 1.0//凹凸度,为0时意味着法线贴图对原法线无影响
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;//偏移/缩放值
sampler2D _BumpMap;
float4 _BumpMap_ST;//偏移/缩放值
float _BumpScale;
fixed4 _Specular;
float _Gloss; struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;//TANGENT是表示切线方向的语义,float4类型,因为需要使用tangent.w分量来计算副切线的方向
float4 texcoord : TEXCOORD0;
}; struct v2f
{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;//定义为float4类型,xy分量存储_MainTex的纹理坐标,zw分量存储_BumpMap的纹理坐标
float3 lightDir : TEXCOORD1;
float3 viewDir : TEXCOORD2;
}; v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex); o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw; //计算副切线(也叫副法线)
float3 binormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal);
//也可以使用内置宏TANGENT_SPACE_ROTATION
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex));
o.viewDir = mul(rotation,ObjSpaceViewDir(v.vertex));
return o;
} fixed4 frag(v2f i) : SV_Target
{
//归一化
fixed3 tangentLightDir = normalize(i.lightDir);
fixed3 tangentViewDir = normalize(i.viewDir);
//Get the texel in the normal Map
fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
//采样得到切线空间下的法线方向(把法线纹理中的像素值映射称法线向量,再乘_BumpScale控制凹凸程度)
fixed3 tangentNormal;
//If the texture is not marked as "Normal Map"
//tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
//tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy))); //mark the texture as "Normal map", and use build-in function
tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
//计算漫反射项
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
//计算高光反射项
fixed3 halfDir = normalize(tangentViewDir + tangentLightDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(tangentNormal, halfDir)), _Gloss); return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}

在材质的属性中加上纹理贴图和法线贴图



效果如下:(使用Bump Scale值可以改变凹凸程度)

2.2 在世界空间下计算

在顶点着色器中,计算从切线空间到世界空间的变换矩阵,将它转递给片元着色器。

在片元着色器中将法线从切线空间变换至世界空间。

世界空间下计算的代码可以在2.1代码的基础上更改。

首先修改顶点着色器的输出结构体v2f。

我们使用切线、副切线、法线按列摆放形成从切线空间到世界空间的变换矩阵。

矩阵的每一行都分别存储在TtoW0、TtoW1、TtoW2中。

插值寄存器最大支持float4,使用float4,正好利用w分量存储worldPos,可以避免空间浪费。

代码如下:

struct v2f
{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;//定义为float4类型,xy分量存储_MainTex的纹理坐标,zw分量存储_BumpMap的纹理坐标
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
};

修改顶点着色器

            v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex); o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw; float3 worldPos = UnityObjectToClipPos(v.vertex);
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; //切线、副切线、法线按列摆放形成矩阵。矩阵的每一行都存储在TtoW中。
//TtoW的w分量存储worldPos(插值寄存器最大支持float4,使用float4避免空间浪费)
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
return o;
}

修改片元着色器

主要更改法线部分。把法线转换至世界坐标下。

            fixed4 frag(v2f i) : SV_Target
{
//世界空间下坐标
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
//归一化lightDir与viewDir
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos)); fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
bump.xy *= _BumpScale;
bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump))); //计算漫反射项
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));
//计算高光反射项
fixed3 halfDir = normalize(viewDir + lightDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss); return fixed4(ambient + diffuse + specular, 1.0);
}

总共的代码如下:

Shader "Unity Shaders Book/Chapter 7/NormalMapWorldSpace"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_BumpMap ("Normal Map", 2D) = "bump" {}//法线纹理,默认值为自带的bump
_BumpScale ("Bump Scale", Float) = 1.0//凹凸度,为0时意味着法线贴图对原法线无影响
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
Pass
{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#include "Lighting.cginc"
#pragma vertex vert
#pragma fragment frag
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;//偏移/缩放值
sampler2D _BumpMap;
float4 _BumpMap_ST;//偏移/缩放值
float _BumpScale;
fixed4 _Specular;
float _Gloss; struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;//TANGENT是表示切线方向的语义,float4类型,因为需要使用tangent.w分量来计算副切线的方向
float4 texcoord : TEXCOORD0;
}; struct v2f
{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;//定义为float4类型,xy分量存储_MainTex的纹理坐标,zw分量存储_BumpMap的纹理坐标
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
}; v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex); o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw; float3 worldPos = UnityObjectToClipPos(v.vertex);
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; //切线、副切线、法线按列摆放形成矩阵。矩阵的每一行都存储在TtoW中。
//TtoW的w分量存储worldPos(插值寄存器最大支持float4,使用float4避免空间浪费)
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
return o;
} fixed4 frag(v2f i) : SV_Target
{
//世界空间下坐标
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
//归一化lightDir与viewDir
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos)); fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
bump.xy *= _BumpScale;
bump.z = sqrt(1.0 - saturate(dot(bump.xy, bump.xy)));
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump))); //计算漫反射项
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));
//计算高光反射项
fixed3 halfDir = normalize(viewDir + lightDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss); return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}

最终实现的效果与2.1相似。

Unity5以后,内置的Shader都使用了世界空间来进行光照计算。

【Unity Shader学习笔记】Unity基础纹理-法线贴图的更多相关文章

  1. 【Unity Shader学习笔记】Unity基础纹理-单张纹理

    1 单张纹理 1.1 纹理 使用纹理映射(Texture Mapping)技术,我们把一张图片逐纹素(Texel)地控制模型的颜色. 美术人员建模时,会在建模软件中利用纹理展开技术把纹理映射坐标(Te ...

  2. 【Unity Shader学习笔记】Unity基础纹理-渐变纹理

    纹理可以用来存储任何表面属性. 可以通过使用渐变纹理来实现插画风格的渲染效果. 这项技术是由Valve公司提出的.Valve使用它来渲染游戏中具有插画风格的角色. 我们使用半兰伯特模型计算漫反射. 因 ...

  3. Unity Shader学习笔记-1

    本篇文章是对Unity Shader入门精要的学习笔记,插图大部分来自冯乐乐女神的github 如果有什么说的不正确的请批评指正 目录 渲染流水线 流程图 Shader作用 屏幕映射 三角形遍历 两大 ...

  4. Unity Shader 学习笔记(一)

    _MainTex_ST (1)简单来说,TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)主要作用是拿顶点的uv去和材质球的t ...

  5. 【Unity Shader学习笔记】Unity光照基础-漫反射光照

    本代码只适用于平行光. 1.逐顶点漫反射光照 1.1漫反射光照原理 1.2代码实现 在Properties语义块中声明一个漫反射颜色属性 Properties { //漫反射参数,用于调整漫反射效果 ...

  6. 【Unity Shader学习笔记】Unity光照基础-高光反射

    1.原理 1.1.Phong模型 计算高光反射需要表面法线.视角方向.光源方向.反射方向等. 在这四个矢量中,我们实际上只需要知道其中3个矢量即可,而第4个矢量(反射方向r)可以通过其他信息计算得到: ...

  7. 【Unity Shader学习笔记】Unity光照基础-半兰伯特光照

    在光照无法达到的区域,模型的外观通常是全黑的,没有任何明暗变化,这会使模型的背光区域看起来就像一个平面. 使用半兰伯特光照可以解决这个问题. 逐顶点光照技术也被称为兰伯特光照模型.因为它符合兰伯特定律 ...

  8. Unity Shader学习笔记 - 用UV动画实现沙滩上的泡沫

    这个泡沫效果来自远古时代的Unity官方海岛Demo, 原效果直接复制3个材质球在js脚本中做UV动画偏移,这里尝试在shader中做动画并且一个pass中完成: // Upgrade NOTE: r ...

  9. unity shader学习笔记(1) shader基础结构以及Properties面板

    首先是shader的基础结构: Shader "Custom/Example { Properties//变量属性面板 { } SubShader { Tags { "Render ...

随机推荐

  1. token的工作原理及其功能

    一.前言 登录模块是我们在前端项目中一定会有的模块,模块中有一个重要的部分是用户登录验证,对于验证用户是否登录过,我们直接处理办法是检查缓存中是否存在token值,若存在则用户可直接登录,反之,用户需 ...

  2. 谷歌开发者工具 Network:Disable cache 和 Preserve log

    参考博文地址:https://my.oschina.net/af666/blog/871793 Network Disable cache(禁止缓存):勾上,修改代码之后,刷新页面没有更新,看有没有禁 ...

  3. 93. 复原 IP 地址

    做题思路or感想 这种字符串切割的问题都可以用回溯法来解决 递归三部曲: 递归参数 因为要切割字符串,所以要用一个startIndex来控制子串的开头位置,即是会切割出一个范围是[startIndex ...

  4. Static in C++

    Static in C++ static根据上下文会有两种含义,他们的区别如下 **在类class或者是在结构体struct 外 **使用static 类外的static修饰的符号在link阶段是局部 ...

  5. Blazor组件自做三 : 使用JS隔离封装ZXing扫码

    Blazor组件自做三 : 使用JS隔离封装ZXing扫码 本文基础步骤参考前两篇文章 Blazor组件自做一 : 使用JS隔离封装viewerjs库 Blazor组件自做二 : 使用JS隔离制作手写 ...

  6. Win7运行net5 wpf条件

    Win7运行net5 wpf条件 win7 sp1 dotnet-runtime-5 vc_redist KB2999226 KB4457144 Tips:官网条件最后一个最坑爹,KB2533623不 ...

  7. 帝国CMS 后台登录空白

    编辑/e/config/config.php中 $ecms_config['esafe']['ckfromurl']=0; //是否启用来源地址验证,0为不验证,1为全部验证,2为后台验证,3为前台验 ...

  8. SpringBoot内外部配置文件加载和优先级

    直接附链接:https://www.pianshen.com/article/28711537583/

  9. Linux shell中2>&1的含义解释

    https://blog.csdn.net/zhaominpro/article/details/82630528

  10. Go Slice Tricks Cheat Sheet、Go 切片使用小妙招

    AppendVector. Copy. Cut. Delete. Delete without preserving order. Cut (GC). Delete (GC). Delete with ...