Unity-Shader详解-其五
关于Unity的Shader部分的基础知识其实已经讲解得差不多了,今天我们来一些实例分享:
溶解
效果如下:
代码如下:
Shader "Chapter8/chapter8_1"
{Properties{// 定义属性[NoScaleOffset]_Albedo("Albedo", 2D) = "white" {} // 基础颜色纹理,默认白色_Noise("Dissolve Noise", 2D) = "white" {} // 溶解噪声纹理,默认白色_Dissolve("Dissolve", Range(0, 1)) = 0 // 溶解程度,范围0到1,默认0[NoScaleOffset]_Gradient("Edge Gradient", 2D) = "black" {} // 边缘渐变纹理,默认黑色_Range("Edge Range", Range(2, 100)) = 6 // 边缘范围,范围2到100,默认6_Brightness("Brightness", Range(0, 10)) = 1 // 亮度,范围0到10,默认1}SubShader{Tags{ "RenderType"="TransparentCutout" "Queue" = "AlphaTest" // 渲染类型为透明剪切,队列为Alpha测试}CGPROGRAM#pragma surface surf StandardSpecular addshadow fullforwardshadows // 使用StandardSpecular表面着色器,并添加阴影和全向阴影struct Input{float2 uv_Albedo; // Albedo纹理的UV坐标float2 uv_Noise; // 噪声纹理的UV坐标};sampler2D _Albedo; // Albedo纹理sampler2D _Noise; // 噪声纹理fixed _Dissolve; // 溶解程度sampler2D _Gradient; // 边缘渐变纹理float _Range; // 边缘范围float _Brightness; // 亮度void surf (Input IN, inout SurfaceOutputStandardSpecular o){// 溶解遮罩fixed noise = tex2D(_Noise, IN.uv_Noise).r; // 从噪声纹理中获取红色通道值fixed dissolve = _Dissolve * 2 - 1; // 将溶解程度从0-1映射到-1到1fixed mask = saturate(noise - dissolve); // 计算遮罩值,限制在0到1之间clip(mask - 0.5); // 根据遮罩值进行剪切,小于0.5的部分将被剔除// 燃烧效果fixed texcoord = saturate(mask * _Range - 0.5 * _Range); // 计算纹理坐标,用于边缘渐变o.Emission = tex2D(_Gradient, fixed2(texcoord, 0.5)) * _Brightness; // 根据渐变纹理和亮度计算自发光fixed4 c = tex2D (_Albedo, IN.uv_Albedo); // 从Albedo纹理中获取颜色o.Albedo = c.rgb; // 设置表面颜色}ENDCG}
}
属性中有一个[NoScaleOffset]:
一言以蔽之, [NoScaleOffset]修饰的纹理的尺寸不可修改(至少在Inspector界面不可修改)。
Tags{ "RenderType"="TransparentCutout" "Queue" = "AlphaTest" // 渲染类型为透明剪切,队列为Alpha测试}
Tags中设置渲染类型为TransparentCutout而渲染队列为AlphaTest。
#pragma surface surf StandardSpecular addshadow fullforwardshadows // 使用StandardSpecular表面着色器,并添加阴影和全向阴影
正如注释所写,使用表面着色器,相关函数为surf,采用StandardSpecular光照模型,添加阴影以及全向阴影。
如果我们需要去查询Unity内置的光照模型和阴影模型可以:
struct Input{float2 uv_Albedo; // Albedo纹理的UV坐标float2 uv_Noise; // 噪声纹理的UV坐标};
这是作为输入的结构体,内部包含的是基础的纹理的UV坐标和噪声纹理的UV纹理。
void surf (Input IN, inout SurfaceOutputStandardSpecular o){// 溶解遮罩fixed noise = tex2D(_Noise, IN.uv_Noise).r; // 从噪声纹理中获取红色通道值fixed dissolve = _Dissolve * 2 - 1; // 将溶解程度从0-1映射到-1到1fixed mask = saturate(noise - dissolve); // 计算遮罩值,限制在0到1之间clip(mask - 0.5); // 根据遮罩值进行剪切,小于0.5的部分将被剔除// 燃烧效果fixed texcoord = saturate(mask * _Range - 0.5 * _Range); // 计算纹理坐标,用于边缘渐变o.Emission = tex2D(_Gradient, fixed2(texcoord, 0.5)) * _Brightness; // 根据渐变纹理和亮度计算自发光fixed4 c = tex2D (_Albedo, IN.uv_Albedo); // 从Albedo纹理中获取颜色o.Albedo = c.rgb; // 设置表面颜色}
我们实现了两个效果:溶解的遮罩,我们获取噪声纹理的红色通道值之后减去溶解的程度值来作为遮罩值,遮罩值小于0.5的部分将会被剔除。
然后是边缘的燃烧效果,我们利用之前生成的遮罩值来计算纹理坐标之后再根据纹理坐标来乘以自发光强度实现渐变纹理。最后我们从基础纹理中获取颜色后再添加到输出的模型中。
透视
效果如图:
能够看到我们可以透过岩石看到人物的轮廓。
代码如下:
Shader "Chapter8/chapter8_2"
{Properties{// 定义属性[Header(The Blocked Part)] // 标题,表示以下属性是用于被遮挡部分的设置[Space(10)] // 在Inspector中留出10像素的空白_Color ("X-Ray Color", Color) = (0,1,1,1) // X射线颜色,默认青色_Width ("X-Ray Width", Range(1, 2)) = 1 // X射线宽度,范围1到2,默认1_Brightness ("X-Ray Brightness",Range(0, 2)) = 1 // X射线亮度,范围0到2,默认1}SubShader{Tags{"RenderType" = "Opaque" "Queue" = "Geometry"} // 渲染类型为不透明,队列为几何体//---------- 被遮挡部分的效果 ----------Pass{ZTest Greater // 深度测试设置为大于当前深度值时才渲染(即渲染被遮挡的部分)ZWrite Off // 关闭深度写入,避免影响后续渲染Blend SrcAlpha OneMinusSrcAlpha // 设置混合模式,实现透明效果CGPROGRAM#pragma vertex vert // 顶点着色器#pragma fragment frag // 片段着色器#include "UnityCG.cginc" // 引入Unity的CG库// 定义顶点着色器的输出结构struct v2f{float4 vertexPos : SV_POSITION; // 顶点在裁剪空间中的位置float3 viewDir : TEXCOORD0; // 视线方向float3 worldNor : TEXCOORD1; // 世界空间中的法线方向};// 顶点着色器v2f vert(appdata_base v){v2f o;o.vertexPos = UnityObjectToClipPos(v.vertex); // 将顶点从对象空间转换到裁剪空间o.viewDir = normalize(WorldSpaceViewDir(v.vertex)); // 计算视线方向o.worldNor = UnityObjectToWorldNormal(v.normal); // 将法线从对象空间转换到世界空间return o;}// 声明属性变量fixed4 _Color; // X射线颜色fixed _Width; // X射线宽度half _Brightness; // X射线亮度// 片段着色器float4 frag(v2f i) : SV_Target{//计算边缘光强度half NDotV = saturate(dot(i.worldNor, i.viewDir)); // 计算法线与视线方向的点积NDotV = pow(1 - NDotV, _Width) * _Brightness; // 根据宽度和亮度调整边缘光强度fixed4 color;color.rgb = _Color.rgb; // 设置颜色color.a = NDotV; // 设置透明度(基于Fresnel值)return color; // 返回最终颜色}ENDCG}}
}
// 定义属性[Header(The Blocked Part)] // 标题,表示以下属性是用于被遮挡部分的设置[Space(10)] // 在Inspector中留出10像素的空白_Color ("X-Ray Color", Color) = (0,1,1,1) // X射线颜色,默认青色_Width ("X-Ray Width", Range(1, 2)) = 1 // X射线宽度,范围1到2,默认1_Brightness ("X-Ray Brightness",Range(0, 2)) = 1 // X射线亮度,范围0到2,默认1
首先是属性中,[Space(10)]在Inspector中留出10像素的空白,效果如图。
Tags{"RenderType" = "Opaque" "Queue" = "Geometry"}
Tags中渲染类型为不透明,队列为几何体。
ZTest Greater // 深度测试设置为大于当前深度值时才渲染(即渲染被遮挡的部分)ZWrite Off // 关闭深度写入,避免影响后续渲染
开启深度测试的同时关闭深度写入。
// 定义顶点着色器的输出结构struct v2f{float4 vertexPos : SV_POSITION; // 顶点在裁剪空间中的位置float3 viewDir : TEXCOORD0; // 视线方向float3 worldNor : TEXCOORD1; // 世界空间中的法线方向};
顶点着色器的输出中多了一个视线方向。
// 顶点着色器v2f vert(appdata_base v){v2f o;o.vertexPos = UnityObjectToClipPos(v.vertex); // 将顶点从对象空间转换到裁剪空间o.viewDir = normalize(WorldSpaceViewDir(v.vertex)); // 计算视线方向o.worldNor = UnityObjectToWorldNormal(v.normal); // 将法线从对象空间转换到世界空间return o;}
这里的视线方向计算方法:
最后是片元着色器的内容:
// 片段着色器float4 frag(v2f i) : SV_Target{//计算边缘光强度half NDotV = saturate(dot(i.worldNor, i.viewDir)); // 计算法线与视线方向的点积NDotV = pow(1 - NDotV, _Width) * _Brightness; // 根据宽度和亮度调整边缘光强度fixed4 color;color.rgb = _Color.rgb; // 设置颜色color.a = NDotV; // 设置透明度(基于Fresnel值)return color; // 返回最终颜色}
这里的边缘光强度计算的内容可能比较难以理解,我们先用法线和视线方向进行一个点积之后调整该值到[0,1]之间,然后根据宽度和亮度来调整光强,这里我们采用了幂次计算,宽度作为幂,那么宽度值越小则光强越小,同时注意我们的base是一减去点积,意思就是法线和视线的夹角越大则光强越强(夹角越大则越边缘),我们通过这些函数实现了边缘光越边缘强度越大的效果。
切割
效果如图:
非常直接的切割效果,代码如下:
Shader "Chapter8/chapter8_3"
{Properties{// 纹理部分[Header(Textures)] [Space(10)] // 标题和空白[NoScaleOffset] _Albedo ("Albedo", 2D) = "white" {} // 基础颜色纹理,默认白色,无缩放偏移// 切割部分[Header(Cutting)] [Space(10)] // 标题和空白[KeywordEnum(X, Y, Z)] _Direction ("Cutting Direction", Float) = 1 // 切割方向枚举(X、Y、Z),默认Y轴[Toggle] _Invert ("Invert Direction", Float) = 0 // 是否反转切割方向,默认关闭}SubShader{Tags { "RenderType"="TransparentCutout" "Queue"="AlphaTest" } // 渲染类型为透明剪切,队列为Alpha测试Cull Off // 关闭背面剔除,渲染双面CGPROGRAM#pragma surface surf StandardSpecular addshadow fullforwardshadows // 使用StandardSpecular表面着色器,添加阴影和全向阴影#pragma target 3.0 // 目标着色器模型3.0#pragma multi_compile _DIRECTION_X _DIRECTION_Y _DIRECTION_Z // 多编译选项,支持X、Y、Z三个方向的切割sampler2D _Albedo; // 基础颜色纹理float3 _Position; // 切割位置fixed _Invert; // 是否反转切割方向struct Input{float2 uv_Albedo; // 基础颜色纹理的UV坐标float3 worldPos; // 世界空间中的顶点位置fixed face : VFACE; // 判断当前渲染的是正面还是背面};void surf (Input i, inout SurfaceOutputStandardSpecular o){// 获取基础颜色fixed4 col = tex2D(_Albedo, i.uv_Albedo);// 如果是正面,使用纹理颜色;如果是背面,使用黑色o.Albedo = i.face > 0 ? col.rgb : fixed3(0,0,0);// 判断切割方向#if _DIRECTION_X// 如果选择X轴方向,根据世界坐标的X值与切割位置比较col.a = step(_Position.x, i.worldPos.x);#elif _DIRECTION_Y// 如果选择Y轴方向,根据世界坐标的Y值与切割位置比较col.a = step(_Position.y, i.worldPos.y);#else // 如果选择Z轴方向,根据世界坐标的Z值与切割位置比较col.a = step(_Position.z, i.worldPos.z);#endif// 判断是否反转切割方向col.a = _Invert? 1 - col.a : col.a;// 根据透明度进行剪切,小于0.001的部分将被剔除clip(col.a - 0.001);}ENDCG}
}
{// 纹理部分[Header(Textures)] [Space(10)] // 标题和空白[NoScaleOffset] _Albedo ("Albedo", 2D) = "white" {} // 基础颜色纹理,默认白色,无缩放偏移// 切割部分[Header(Cutting)] [Space(10)] // 标题和空白[KeywordEnum(X, Y, Z)] _Direction ("Cutting Direction", Float) = 1 // 切割方向枚举(X、Y、Z),默认Y轴[Toggle] _Invert ("Invert Direction", Float) = 0 // 是否反转切割方向,默认关闭}
这里有两个新东西:KeywordEnum和一个Toggle。
效果如下:
struct Input{float2 uv_Albedo; // 基础颜色纹理的UV坐标float3 worldPos; // 世界空间中的顶点位置fixed face : VFACE; // 判断当前渲染的是正面还是背面};
作为输入的结构体里除了基本的纹理UV坐标和顶点坐标以外还有一个声明为VFACE的face变量,用来表明具体渲染的是正面还是背面。
void surf (Input i, inout SurfaceOutputStandardSpecular o){// 获取基础颜色fixed4 col = tex2D(_Albedo, i.uv_Albedo);// 如果是正面,使用纹理颜色;如果是背面,使用黑色o.Albedo = i.face > 0 ? col.rgb : fixed3(0,0,0);// 判断切割方向#if _DIRECTION_X// 如果选择X轴方向,根据世界坐标的X值与切割位置比较col.a = step(_Position.x, i.worldPos.x);#elif _DIRECTION_Y// 如果选择Y轴方向,根据世界坐标的Y值与切割位置比较col.a = step(_Position.y, i.worldPos.y);#else // 如果选择Z轴方向,根据世界坐标的Z值与切割位置比较col.a = step(_Position.z, i.worldPos.z);#endif// 判断是否反转切割方向col.a = _Invert? 1 - col.a : col.a;// 根据透明度进行剪切,小于0.001的部分将被剔除clip(col.a - 0.001);}
表面着色器里,我们首先获取纹理的颜色,然后先判断是正面还是背面,接着从X,Y,Z轴选择切割的方向,根据选择的轴来确定世界坐标和切割位置的比较。这里我们使用了一个函数step来进行比较,step的具体用法如下:
最后把小于阈值的部分直接剔除掉即可。
切割轴为Y轴时效果如图:
广告
效果如下:
就是实现无论哪个位置看到的图片效果都一样。
代码如下:
Shader "Chapter8/chapter8_4"
{Properties{[NoScaleOffset] _Tex ("Texture", 2D) = "white" {}[KeywordEnum(Spherical, Cylindrical)] _Type ("Type", float) = 0}SubShader{Tags{"RenderType" = "Transparent""Queue" = "Transparent""DisableBatching" = "True"}//Blend OneMinusDstColor OneZWrite OffPass{CGPROGRAM#pragma vertex vert#pragma fragment frag// 声明枚举的关键词#pragma shader_feature _TYPE_SPHERICAL _TYPE_CYLINDRICALstruct appdata{float4 vertex : POSITION;float2 texcoord : TEXCOORD0;};struct v2f{float4 vertex : SV_POSITION;float2 texcoord : TEXCOORD0;};sampler2D _Tex;v2f vert (appdata v){v2f o;// 计算面片朝向摄像机的前方向量float3 forward = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos, 1)).xyz;// 判断Billboard的类型#if _TYPE_CYLINDRICALforward.y = 0;#endifforward = normalize(forward);// 当摄像机完全在面片正上方或者正下方的时候,旋转临时的上方向量float3 up = abs(forward.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);float3 right = normalize(cross(forward, up));up = normalize(cross(right, forward));// 将顶点在新的坐标系上移动位置float3 vertex = v.vertex.x * right + v.vertex.y * up;o.vertex = UnityObjectToClipPos(vertex);o.texcoord = v.texcoord;return o;}float4 frag (v2f i) : SV_Target{return tex2D(_Tex, i.texcoord);}ENDCG}}
}
Tags{"RenderType" = "Transparent""Queue" = "Transparent""DisableBatching" = "True"}
这次的Tags里有新东西DisableBatching:
总结来说就是,批处理可以减少draw call但是会导致顶点坐标从模型空间转换为世界空间,所以如果在后续的代码中我们要使用模型空间的坐标的话就无法使用,所以我们需要显式地禁止使用批处理。
// 声明枚举的关键词#pragma shader_feature _TYPE_SPHERICAL _TYPE_CYLINDRICAL
比起往常的shader多了一个shader_feature。
shader_feature本质上更像一个shader代码里的宏定义,我们可以根据不同的宏定义替换不同的shader变体。
v2f vert (appdata v){v2f o;// 计算面片朝向摄像机的前方向量float3 forward = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos, 1)).xyz;// 判断Billboard的类型#if _TYPE_CYLINDRICALforward.y = 0;#endifforward = normalize(forward);// 当摄像机完全在面片正上方或者正下方的时候,旋转临时的上方向量float3 up = abs(forward.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);float3 right = normalize(cross(forward, up));up = normalize(cross(right, forward));// 将顶点在新的坐标系上移动位置float3 vertex = v.vertex.x * right + v.vertex.y * up;o.vertex = UnityObjectToClipPos(vertex);o.texcoord = v.texcoord;return o;}
我们将摄像机的世界空间坐标从世界坐标系转换到模型空间坐标系,获得该面片朝向摄像机的方向向量。如果是圆柱形的广告牌,我们将这个方向向量的y轴修改为0。否则如果该方向向量的y轴分量大于0.999(基本就是在纯上方)我们把向上的向量直接设置为z轴方向向量(此时forward向量和向上向量高度重合,叉乘大概率为0,后续计算无法展开),否则就设置为y轴方向向量,然后用前向向量和上方向量叉乘得到右侧向量,之后再用右向向量和前方向量叉乘得到上方向量。
最后我们把v的顶点坐标和纹理坐标都根据这个右向向量和上方向量更新之后即可,这样我们就实现了一个面片永远朝向摄像机的效果。
扭曲
效果如下:
可以看到这些形形色色的颜色球在的位置视线被扭曲了。
代码如下:
Shader "Chapter8/chapter8_5"
{Properties{_StrengthColor("Color strength", Float) = 1 // 颜色强度,默认1_DistortionStrength ("Distortion strength", Range(-2,2)) = 0.1 // 扭曲强度,范围-2到2,默认0.1_DistortionCircle ("Distortion circle", Range(0,1)) = 0 // 扭曲圆形范围,范围0到1,默认0_NormalTexture("Normal", 2D) = "blue" { } // 法线纹理,默认蓝色_NormalTexStrength("Normal strength", Range(0,1)) = 0.5 // 法线强度,范围0到1,默认0.5_NormalTexFrameless("Normal circle", Range(0,1)) = 0.5 // 法线圆形范围,范围0到1,默认0.5_UVOffset("UVOffset XY, ignore ZW", Vector) = (0,0.01,0,0) // UV偏移,默认(0, 0.01, 0, 0)}Category{Tags { "Queue" = "Transparent" "RenderType" = "Transparent" "IgnoreProjector" = "True" } // 渲染队列为透明,渲染类型为透明,忽略投影器Blend SrcAlpha OneMinusSrcAlpha // 混合模式:SrcAlpha, OneMinusSrcAlphaZWrite Off // 关闭深度写入SubShader{GrabPass // 抓取屏幕内容{Name "BASE"Tags { "LightMode" = "Always" }}Pass{Name "BASE"Tags { "LightMode" = "Always" }CGPROGRAM#pragma vertex vert // 顶点着色器#pragma fragment frag // 片段着色器#include "UnityCG.cginc" // 引入Unity的CG库sampler2D _GrabTexture; // 抓取的屏幕纹理float _DistortionStrength; // 扭曲强度float _DistortionCircle; // 扭曲圆形范围float _StrengthColor; // 颜色强度sampler2D _NormalTexture; // 法线纹理float4 _NormalTexture_ST; // 法线纹理的缩放和偏移float _NormalTexStrength; // 法线强度float _NormalTexFrameless; // 法线圆形范围float4 _UVOffset; // UV偏移// 顶点着色器输入结构struct VertexInput{float4 vertex : POSITION; // 顶点位置float2 texcoord0 : TEXCOORD0; // 纹理坐标float4 color : COLOR; // 顶点颜色};// 顶点着色器输出结构struct Vert2Frag{float4 position : SV_POSITION; // 裁剪空间中的顶点位置float4 uv_grab : TEXCOORD0; // 抓取纹理的UV坐标float2 uv : TEXCOORD1; // 纹理坐标float2 uv_normal : TEXCOORD2; // 法线纹理的UV坐标float2 movement: TEXCOORD3; // UV偏移运动float4 color : TEXCOORD4; // 顶点颜色};// 顶点着色器Vert2Frag vert (VertexInput vertIn){Vert2Frag output;output.position = UnityObjectToClipPos(vertIn.vertex); // 将顶点从对象空间转换到裁剪空间output.uv_grab = ComputeGrabScreenPos(output.position); // 计算抓取纹理的UV坐标output.uv = vertIn.texcoord0; // 传递纹理坐标output.uv_normal = vertIn.texcoord0.xy * _NormalTexture_ST.xy + _NormalTexture_ST.zw; // 计算法线纹理的UV坐标output.movement = _UVOffset.xy*_Time.y; // 计算UV偏移运动output.color = vertIn.color; // 传递顶点颜色return output;}// 获取从中心到当前UV的向量float2 getVectorFromCenter(float2 uv){float factor = _ScreenParams.y / _ScreenParams.x; // 计算屏幕宽高比float2 direction = float2((uv.x-0.5), (uv.y-0.5)) * factor; // 计算从中心到当前UV的向量return (direction);}// 获取扭曲强度float getDistortionStrength(float2 uv){float2 diff = float2(distance(0.5, uv.x), distance(0.5, uv.y)) * 2.0; // 计算UV到中心的距离float dist = saturate(length(diff)); // 计算距离并限制在0到1之间return 1.0-dist; // 返回扭曲强度}// 获取法线float2 getNormal(sampler2D _NormalTexture, float2 normalUv, float2 uv, float2 uvOffset, float frameless, float strength){float2 normal = tex2D( _NormalTexture, normalUv+uvOffset ).zy; // 从法线纹理中获取法线值float length = getDistortionStrength(uv); // 获取扭曲强度float normalTexStrength = ((1-frameless) + frameless*length) * strength; // 计算法线强度normal.x = ((normal.x-.5)*2) * normalTexStrength; // 调整法线X分量normal.y = ((normal.y-.5)*2) * normalTexStrength; // 调整法线Y分量return normal; // 返回法线}// 片段着色器half4 frag (Vert2Frag fragIn) : SV_Target{float4 uvScreen = UNITY_PROJ_COORD(fragIn.uv_grab); // 获取抓取纹理的UV坐标float2 direction = getVectorFromCenter(fragIn.uv); // 获取从中心到当前UV的向量float strength = getDistortionStrength(fragIn.uv); // 获取扭曲强度strength = (_DistortionCircle*strength + (1-_DistortionCircle)) * _DistortionStrength; // 计算最终扭曲强度direction *= strength; // 调整方向向量uvScreen += float4(direction.x, direction.y, 0, 0); // 调整抓取纹理的UV坐标float2 influence = normalize(direction) * strength; // 计算影响向量float2 offset = fragIn.movement; // 获取UV偏移float2 normal = getNormal(_NormalTexture, fragIn.uv_normal, fragIn.uv, offset, _NormalTexFrameless, _NormalTexStrength); // 获取法线uvScreen += float4(normal.x, normal.y, 0, 0); // 调整抓取纹理的UV坐标influence += normal.xy; // 调整影响向量float4 final = tex2Dproj(_GrabTexture, uvScreen); // 从抓取纹理中获取颜色float alpha = 1; // 设置透明度final = float4(final.xyz, alpha); // 设置最终颜色strength = saturate(sqrt(pow(abs(influence.x), 2.0) + pow(abs(influence.y), 2.0)) * _StrengthColor); // 计算最终强度final = final + (fragIn.color*strength); // 调整最终颜色final.w = saturate(final.w*fragIn.color.w); // 调整最终透明度return final; // 返回最终颜色}ENDCG}}}
}
我们应该首先能发现这一次的shader中没有Pass而是Category:
GrabPass // 抓取屏幕内容{Name "BASE"Tags { "LightMode" = "Always" }}
如果还记得我们透明章节的话,我们在那里介绍过GrabPass:从屏幕中抓取缓冲来使用。
// 顶点着色器输入结构struct VertexInput{float4 vertex : POSITION; // 顶点位置float2 texcoord0 : TEXCOORD0; // 纹理坐标float4 color : COLOR; // 顶点颜色};// 顶点着色器输出结构struct Vert2Frag{float4 position : SV_POSITION; // 裁剪空间中的顶点位置float4 uv_grab : TEXCOORD0; // 抓取纹理的UV坐标float2 uv : TEXCOORD1; // 纹理坐标float2 uv_normal : TEXCOORD2; // 法线纹理的UV坐标float2 movement: TEXCOORD3; // UV偏移运动float4 color : TEXCOORD4; // 顶点颜色};
顶点着色器的输入和输出都有很多东西,把顶点位置、纹理坐标和顶点颜色作为输入而输出裁剪空间的顶点位置、抓取的纹理坐标、纹理坐标、法线纹理坐标、UV的偏移和颜色。
// 顶点着色器Vert2Frag vert (VertexInput vertIn){Vert2Frag output;output.position = UnityObjectToClipPos(vertIn.vertex); // 将顶点从对象空间转换到裁剪空间output.uv_grab = ComputeGrabScreenPos(output.position); // 计算抓取纹理的UV坐标output.uv = vertIn.texcoord0; // 传递纹理坐标output.uv_normal = vertIn.texcoord0.xy * _NormalTexture_ST.xy + _NormalTexture_ST.zw; // 计算法线纹理的UV坐标output.movement = _UVOffset.xy*_Time.y; // 计算UV偏移运动output.color = vertIn.color; // 传递顶点颜色return output;}
这是顶点着色器的函数代码内容。
// 获取从中心到当前UV的向量float2 getVectorFromCenter(float2 uv){float factor = _ScreenParams.y / _ScreenParams.x; // 计算屏幕宽高比float2 direction = float2((uv.x-0.5), (uv.y-0.5)) * factor; // 计算从中心到当前UV的向量return (direction);}// 获取扭曲强度float getDistortionStrength(float2 uv){float2 diff = float2(distance(0.5, uv.x), distance(0.5, uv.y)) * 2.0; // 计算UV到中心的距离float dist = saturate(length(diff)); // 计算距离并限制在0到1之间return 1.0-dist; // 返回扭曲强度}// 获取法线float2 getNormal(sampler2D _NormalTexture, float2 normalUv, float2 uv, float2 uvOffset, float frameless, float strength){float2 normal = tex2D( _NormalTexture, normalUv+uvOffset ).zy; // 从法线纹理中获取法线值float length = getDistortionStrength(uv); // 获取扭曲强度float normalTexStrength = ((1-frameless) + frameless*length) * strength; // 计算法线强度normal.x = ((normal.x-.5)*2) * normalTexStrength; // 调整法线X分量normal.y = ((normal.y-.5)*2) * normalTexStrength; // 调整法线Y分量return normal; // 返回法线}
这里有三个函数,分别用于计算从中心到当前UV的向量,计算扭曲强度以及获取法线。
第一个函数,我们首先计算一个屏幕的高宽比,然后把uv的x轴和y轴各减去0.5之后乘以这个比值,因为我们知道uv坐标是一个从0到1的坐标系,所以这样能得到正确的向量。
第二个函数中,我们先计算uv坐标的xy坐标到屏幕的中心的距离,然后把这个距离限制在[0,1]之间,最后返回一减去这个距离即可。
第三个函数中,我们首先从法线纹理中获取到法线,然后使用第二个函数获取扭曲强度,然后我们根据扭曲强度和参数中的边缘衰减(frameless)来动态调整法线强度。
// 片段着色器half4 frag (Vert2Frag fragIn) : SV_Target{float4 uvScreen = UNITY_PROJ_COORD(fragIn.uv_grab); // 获取抓取纹理的UV坐标float2 direction = getVectorFromCenter(fragIn.uv); // 获取从中心到当前UV的向量float strength = getDistortionStrength(fragIn.uv); // 获取扭曲强度strength = (_DistortionCircle*strength + (1-_DistortionCircle)) * _DistortionStrength; // 计算最终扭曲强度direction *= strength; // 调整方向向量uvScreen += float4(direction.x, direction.y, 0, 0); // 调整抓取纹理的UV坐标float2 influence = normalize(direction) * strength; // 计算影响向量float2 offset = fragIn.movement; // 获取UV偏移float2 normal = getNormal(_NormalTexture, fragIn.uv_normal, fragIn.uv, offset, _NormalTexFrameless, _NormalTexStrength); // 获取法线uvScreen += float4(normal.x, normal.y, 0, 0); // 调整抓取纹理的UV坐标influence += normal.xy; // 调整影响向量float4 final = tex2Dproj(_GrabTexture, uvScreen); // 从抓取纹理中获取颜色float alpha = 1; // 设置透明度final = float4(final.xyz, alpha); // 设置最终颜色strength = saturate(sqrt(pow(abs(influence.x), 2.0) + pow(abs(influence.y), 2.0)) * _StrengthColor); // 计算最终强度final = final + (fragIn.color*strength); // 调整最终颜色final.w = saturate(final.w*fragIn.color.w); // 调整最终透明度return final; // 返回最终颜色}
我们的片元着色器就是重点了,可以看到很多内容啊,我们来看看AI怎么说吧:
扫描
这是一个更为复杂的项目,效果如下:
我们实现的效果是鼠标点击某个地点之后会发射这样一个扫描的波形。
这里就不只是一个shader可以实现的效果了,我们还需要一个C#脚本。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;// 该脚本实现了一个扫描效果,当用户点击物体时,扫描效果会从该位置开始,并随时间扩展
// 这个脚本会在编辑模式下也生效([ExecuteInEditMode]属性)
[ExecuteInEditMode]
public class PosScanEffect : MonoBehaviour
{// 用于存储扫描效果的材质(shader),此材质会控制实际的视觉效果public Material ScanMat;// 控制扫描速度,扫描的范围会根据这个值逐渐增大public float ScanSpeed = 20;// 扫描计时器,决定扫描的进度,随着时间的推移,扫描的范围会增大public float scanTimer = 0;// 存储相机组件的引用private Camera scanCam;// 记录扫描的中心点,即鼠标点击时的物体位置private Vector3 ScanPoint = Vector3.zero;// 初始化方法,这里没有初始化操作void Awake(){}// 每一帧调用,主要用于计算扫描参数和更新扫描效果private void Update(){// 获取当前物体上的相机组件scanCam = GetComponent<Camera>();// 启用深度纹理(Depth)和法线深度纹理(DepthNormals),这些纹理对后续的渲染处理至关重要scanCam.depthTextureMode |= DepthTextureMode.Depth;scanCam.depthTextureMode |= DepthTextureMode.DepthNormals;// 获取相机的长宽比float aspect = scanCam.aspect;// 获取相机的远裁剪平面(远离相机的最大距离)float farPlaneDistance = scanCam.farClipPlane;// 根据相机的视野(field of view)计算出上方向量,用于定位视锥体的边界Vector3 midup = Mathf.Tan(scanCam.fieldOfView / 2 * Mathf.Deg2Rad) * farPlaneDistance * scanCam.transform.up;// 计算右方向量,同样用于确定视锥体的边界Vector3 midright = Mathf.Tan(scanCam.fieldOfView / 2 * Mathf.Deg2Rad) * farPlaneDistance * scanCam.transform.right * aspect;// 计算远裁剪平面的中心点位置Vector3 farPlaneMid = scanCam.transform.forward * farPlaneDistance;// 根据计算出的参数确定视锥体的四个角的世界坐标Vector3 bottomLeft = farPlaneMid - midup - midright;Vector3 bottomRight = farPlaneMid - midup + midright;Vector3 upLeft = farPlaneMid + midup - midright;Vector3 upRight = farPlaneMid + midup + midright;// 创建一个矩阵来表示视锥体的四个角Matrix4x4 frustumCorner = new Matrix4x4();frustumCorner.SetRow(0, bottomLeft); // 设置底左角frustumCorner.SetRow(1, bottomRight); // 设置底右角frustumCorner.SetRow(2, upRight); // 设置上右角frustumCorner.SetRow(3, upLeft); // 设置上左角// 将视锥体的矩阵传递给ShaderScanMat.SetMatrix("_FrustumCorner", frustumCorner);// 进行射线检测,获取鼠标点击的世界坐标RaycastHit hit;Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); // 将鼠标屏幕坐标转换为射线if (Input.GetMouseButton(0) && Physics.Raycast(ray, out hit)) // 如果按下左键并且射线与物体碰撞{scanTimer = 0; // 重置扫描计时器ScanPoint = hit.point; // 获取碰撞点的位置作为扫描的起始点}// 增加扫描计时器,控制扫描的进度scanTimer += Time.deltaTime;// 将扫描的起始位置和扫描进度传递给材质(Shader)ScanMat.SetVector("_ScanCenter", ScanPoint);ScanMat.SetFloat("_ScanRange", scanTimer * ScanSpeed);// 将相机的世界坐标变换矩阵传递给ShaderScanMat.SetMatrix("_CamToWorld", scanCam.cameraToWorldMatrix);}// 在渲染图像时调用,进行后处理操作,应用扫描效果private void OnRenderImage(RenderTexture source, RenderTexture destination){// 将相机的远裁剪平面距离传递给材质(Shader),用于控制扫描的范围ScanMat.SetFloat("_CamFar", GetComponent<Camera>().farClipPlane);// 使用Graphics.Blit方法将源纹理渲染到目标纹理,并应用扫描效果的材质Graphics.Blit(source, destination, ScanMat);}
}
具体的代码作用注释里已经写明了。
我们重点还是来看看着色器的写法:
Shader "Chapter2/PointScanShader"
{Properties{// 主要纹理,用于物体表面的纹理_MainTex ("Texture", 2D) = "white" {}// 扫描纹理,用于显示扫描的图像或效果_ScanTex("ScanTexure", 2D) = "white" {}// 扫描的范围,控制扫描从中心开始的距离_ScanRange("ScanRange", float) = 0// 扫描宽度,控制扫描的宽度,影响扫描的可见区域_ScanWidth("ScanWidth", float) = 0// 扫描的背景颜色_ScanBgColor("ScanBgColor", color) = (1, 1, 1, 1)// 扫描时网格的颜色_ScanMeshColor("ScanMeshColor", color) = (1, 1, 1, 1)// 网格线的宽度_MeshLineWidth("MeshLineWidth", float) = 0.3// 网格的宽度,用于控制网格分割的尺寸_MeshWidth("MeshWidth", float) = 1// 缝隙的平滑度,控制缝隙的过渡效果_Smoothness("SeamBlending", Range(0, 0.5)) = 0.25}SubShader{// 不使用背面剔除(Cull Off),不写入深度缓存(ZWrite Off),且始终通过深度测试(ZTest Always)//Cull Off ZWrite Off ZTest AlwaysPass{CGPROGRAM#pragma vertex vert#pragma fragment frag// 引用Unity的内置CG代码库#include "UnityCG.cginc"// 定义顶点着色器的数据结构struct appdata{float4 vertex : POSITION; // 顶点位置float2 uv : TEXCOORD0; // 顶点纹理坐标};// 定义片段着色器的数据结构struct v2f{float2 uv : TEXCOORD0; // 传递给片段着色器的纹理坐标float2 uv_depth : TEXCOORD1; // 用于传递深度信息的纹理坐标float4 interpolatedRay : TEXCOORD2; // 传递与扫描效果相关的视锥体数据float4 vertex : SV_POSITION; // 顶点最终位置};// 定义一个矩阵,用于描述视锥体四个角的坐标float4x4 _FrustumCorner;// 顶点着色器:计算顶点的最终位置,并计算视锥体四个角的插值数据v2f vert(appdata v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex); // 计算顶点的最终位置o.uv = v.uv; // 将纹理坐标传递给片段着色器o.uv_depth = v.uv; // 将纹理坐标传递给深度计算// 根据UV坐标的不同区域选择不同的视锥体角int rayIndex;if (v.uv.x < 0.5 && v.uv.y < 0.5){rayIndex = 0;}else if (v.uv.x > 0.5 && v.uv.y < 0.5){rayIndex = 1;}else if (v.uv.x > 0.5 && v.uv.y > 0.5){rayIndex = 2;}else{rayIndex = 3;}// 从视锥体四个角中选择一个,传递给片段着色器o.interpolatedRay = _FrustumCorner[rayIndex];return o;}// 声明材质参数,允许外部设置sampler2D _MainTex;sampler2D _ScanTex;float _ScanRange;float _ScanWidth;float3 _ScanCenter;fixed4 _ScanBgColor;fixed4 _ScanMeshColor;float _MeshLineWidth;float _MeshWidth;float4x4 _CamToWorld;fixed _Smoothness;// 声明用于获取深度信息的纹理sampler2D_float _CameraDepthTexture;sampler2D _CameraDepthNormalsTexture;// 片段着色器:计算像素的最终颜色fixed4 frag(v2f i) : SV_Target{float tempDepth;half3 normal; // 获取该像素的法线和深度值DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), tempDepth, normal);// 将法线从相机空间转换到世界空间normal = mul((float3x3)_CamToWorld, normal);normal = normalize(max(0, (abs(normal) - _Smoothness))); // 对法线进行平滑处理// 获取该像素的颜色fixed4 col = tex2D(_MainTex, i.uv);// 通过深度纹理获取该像素的深度值,并转换为线性深度float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);float linearDepth = Linear01Depth(depth);// 计算该像素在世界空间中的位置float3 pixelWorldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay;// 计算像素到扫描中心的距离float pixelDistance = distance(pixelWorldPos, _ScanCenter);// 计算该像素的方向向量float3 pixelDir = pixelWorldPos - _ScanCenter;// 使用网格宽度将像素位置进行取模操作,用于创建网格效果float3 modulo = pixelWorldPos - _MeshWidth * floor(pixelWorldPos / _MeshWidth);modulo = modulo / _MeshWidth;// 使用平滑插值创建网格线效果float3 meshCol = smoothstep(_MeshLineWidth, 0, modulo) + smoothstep(1 - _MeshLineWidth, 1, modulo);// 将扫描背景颜色和网格颜色进行插值fixed4 scanMeshCol = lerp(_ScanBgColor, _ScanMeshColor, saturate(dot(meshCol, 1 - normal)));// 如果像素距离扫描中心在指定范围内,执行扫描效果if (_ScanRange - pixelDistance > 0 && _ScanRange - pixelDistance < _ScanWidth && linearDepth < 1){// 根据像素距离计算扫描百分比fixed scanPercent = 1 - (_ScanRange - pixelDistance) / _ScanWidth;// 通过插值将当前颜色与网格颜色混合,生成扫描效果col = lerp(col, scanMeshCol, scanPercent);}// 返回最终颜色return col;}ENDCG}}
}
Properties{// 主要纹理,用于物体表面的纹理_MainTex ("Texture", 2D) = "white" {}// 扫描纹理,用于显示扫描的图像或效果_ScanTex("ScanTexure", 2D) = "white" {}// 扫描的范围,控制扫描从中心开始的距离_ScanRange("ScanRange", float) = 0// 扫描宽度,控制扫描的宽度,影响扫描的可见区域_ScanWidth("ScanWidth", float) = 0// 扫描的背景颜色_ScanBgColor("ScanBgColor", color) = (1, 1, 1, 1)// 扫描时网格的颜色_ScanMeshColor("ScanMeshColor", color) = (1, 1, 1, 1)// 网格线的宽度_MeshLineWidth("MeshLineWidth", float) = 0.3// 网格的宽度,用于控制网格分割的尺寸_MeshWidth("MeshWidth", float) = 1// 缝隙的平滑度,控制缝隙的过渡效果_Smoothness("SeamBlending", Range(0, 0.5)) = 0.25
一些主要的属性,含义已在注释中写明。
// 定义片段着色器的数据结构struct v2f{float2 uv : TEXCOORD0; // 传递给片段着色器的纹理坐标float2 uv_depth : TEXCOORD1; // 用于传递深度信息的纹理坐标float4 interpolatedRay : TEXCOORD2; // 传递与扫描效果相关的视锥体数据float4 vertex : SV_POSITION; // 顶点最终位置};
顶点着色器的输出以及片元着色器的输入中除了uv坐标和顶点坐标以外还有一个传递深度信息的uv坐标以及一个与扫描效果相关的变量。
// 定义一个矩阵,用于描述视锥体四个角的坐标float4x4 _FrustumCorner;
注意这里定义矩阵的方式:float4*4,这个矩阵用来描述视锥体的四个角。
// 顶点着色器:计算顶点的最终位置,并计算视锥体四个角的插值数据v2f vert(appdata v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex); // 计算顶点的最终位置o.uv = v.uv; // 将纹理坐标传递给片段着色器o.uv_depth = v.uv; // 将纹理坐标传递给深度计算// 根据UV坐标的不同区域选择不同的视锥体角int rayIndex;if (v.uv.x < 0.5 && v.uv.y < 0.5){rayIndex = 0;}else if (v.uv.x > 0.5 && v.uv.y < 0.5){rayIndex = 1;}else if (v.uv.x > 0.5 && v.uv.y > 0.5){rayIndex = 2;}else{rayIndex = 3;}// 从视锥体四个角中选择一个,传递给片段着色器o.interpolatedRay = _FrustumCorner[rayIndex];return o;}
我们根据不同的视锥体的位置来给定一个序号,从这四个序号中选择一个传给片元着色器。
// 片段着色器:计算像素的最终颜色
fixed4 frag(v2f i) : SV_Target
{float tempDepth;half3 normal; // 获取该像素的法线和深度值DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), tempDepth, normal);// 将法线从相机空间转换到世界空间normal = mul((float3x3)_CamToWorld, normal);normal = normalize(max(0, (abs(normal) - _Smoothness))); // 对法线进行平滑处理// 获取该像素的颜色fixed4 col = tex2D(_MainTex, i.uv);// 通过深度纹理获取该像素的深度值,并转换为线性深度float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);float linearDepth = Linear01Depth(depth);// 计算该像素在世界空间中的位置float3 pixelWorldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay;// 计算像素到扫描中心的距离float pixelDistance = distance(pixelWorldPos, _ScanCenter);// 计算该像素的方向向量float3 pixelDir = pixelWorldPos - _ScanCenter;// 使用网格宽度将像素位置进行取模操作,用于创建网格效果float3 modulo = pixelWorldPos - _MeshWidth * floor(pixelWorldPos / _MeshWidth);modulo = modulo / _MeshWidth;// 使用平滑插值创建网格线效果float3 meshCol = smoothstep(_MeshLineWidth, 0, modulo) + smoothstep(1 - _MeshLineWidth, 1, modulo);// 将扫描背景颜色和网格颜色进行插值fixed4 scanMeshCol = lerp(_ScanBgColor, _ScanMeshColor, saturate(dot(meshCol, 1 - normal)));// 如果像素距离扫描中心在指定范围内,执行扫描效果if (_ScanRange - pixelDistance > 0 && _ScanRange - pixelDistance < _ScanWidth && linearDepth < 1){// 根据像素距离计算扫描百分比fixed scanPercent = 1 - (_ScanRange - pixelDistance) / _ScanWidth;// 通过插值将当前颜色与网格颜色混合,生成扫描效果col = lerp(col, scanMeshCol, scanPercent);}// 返回最终颜色return col;
}
首先我们使用DecodeDepthNormal函数从_CameraDepthNormalsTexture中获取到法线和深度值,然后将法线经过一系列处理转换到世界空间。我们将深度值转换成线性深度之后用来计算该像素在世界空间的位置与扫描中心的距离,同时计算这个像素到扫描中心的向量。因为我们要实现网格效果,网格效果是一格一格实现的,所以我们需要将像素的位置取模来确定具体在第几个网格,然后将背景的颜色和网格颜色进行插值实现混合的效果,最终返回颜色。
关于线性深度:
相关文章:

Unity-Shader详解-其五
关于Unity的Shader部分的基础知识其实已经讲解得差不多了,今天我们来一些实例分享: 溶解 效果如下: 代码如下: Shader "Chapter8/chapter8_1" {Properties{// 定义属性[NoScaleOffset]_Albedo("Albedo", 2…...

【Java 专题补充】流程控制语句
流程控制语句是用来控制程序中各语句执行顺序的语句,是程序中既基本又非常关键的部分。流程控制语句可以把单个的语句组合成有意义的、能完成一定功能的小逻辑模块。最主要的流程控制方式是结构化程序设计中规定的三种基本流程结构。 1.1 结构化程序设计的三种基本流…...
恶心的win11更新DIY 设置win11更新为100年
打开注册表编辑器:按下Win R键,输入regedit,然后按回车打开注册表编辑器。12导航到指定路径:在注册表编辑器中,依次展开HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings新建DWORD值&…...

【ArcGIS微课1000例】0146:将多个文件夹下的影像移动到一个目标文件夹(以Landscan数据为例)
本文讲述将多个文件夹下的影像移动到一个目标文件夹,便于投影变换、裁剪等操作。 文章目录 一、数据准备二、解压操作三、批量移动四、查看效果五、ArcGIS操作一、数据准备 全球人口数据集Landscan2000-2023如下所示,每年数据位一个压缩包: 二、解压操作 首先将其解压,方…...

【redis】分片方案
Redis分片(Sharding)是解决单机性能瓶颈的核心技术,其本质是将数据分散存储到多个Redis节点(实例)中,每个实例将只是所有键的一个子集,通过水平扩展提升系统容量和性能。 分片的核心价值 性能提…...

springboot+mysql+element-plus+vue完整实现汽车租赁系统
目录 一、项目介绍 二、项目截图 1.项目结构图 三、系统详细介绍 管理后台 1.登陆页 2.管理后台主页 3.汽车地点管理 4.汽车类别 5.汽车品牌 6.汽车信息 7.用户管理 8.举报管理 9.订单管理 10.轮播图管理 11.交互界面 12.图表管理 汽车租赁商城 1.首页 2.汽…...

Linux第四节:进程控制
一、进程创建 1.1 fork函数 1. fork函数有两个返回值问题 返回的本质就是写入!所以,谁先返回,谁就先写入id,因为进程具有独立性,会发生写时拷贝,父进程和子进程各自指向return语句。 2. fork返回后&#x…...

Qt 编译 sqldrivers之psql
编译postgres pgsql驱动 下载驱动源码修改配置文件编译 下载驱动源码 // 源代码下载 https://download.qt.io/archive/qt/5.15/5.15.2/submodules/驱动目录:qtbase-everywhere-src-5.15.2\src\plugins\sqldrivers 修改配置文件 打开pro文件 右键点击添加库 此处的为debu…...
382_C++_在用户会话结束时,检查是否有其他会话仍然来自同一个客户端 IP 地址,没有连接状态设置为断开,否则为连接
之前出现的问题:重启管理机,工作机上面热备连接状态显示未连接 (此时是有一个工作机连接管理机的),所以正常应该是连接状态解决:根因分析: 重启管理机后,管理机给过来的cookie是空的,导致工作机同时存在两个管理机的session,在其中一个超时后,调用回调函数通知会话断开…...
RViz(机器人可视化工具)的配置文件(moveitcpp)
1. Panels(面板设置) 面板是RViz界面中的各个功能区域,用于显示和操作不同的数据。 Displays(显示面板) Class: rviz_common/Displays 指定面板的类型,这里是显示面板。 Help Height: 78 帮助区域的高度…...
Redis中6种缓存更新策略
Redis作为一款高性能的内存数据库,已经成为缓存层的首选解决方案。然而,使用缓存时最大的挑战在于保证缓存数据与底层数据源的一致性。缓存更新策略直接影响系统的性能、可靠性和数据一致性,选择合适的策略至关重要。 本文将介绍Redis中6种缓…...
如何使用极狐GitLab 软件包仓库功能托管 terraform?
极狐GitLab 是 GitLab 在中国的发行版,关于中文参考文档和资料有: 极狐GitLab 中文文档极狐GitLab 中文论坛极狐GitLab 官网 Terraform 模块库 (BASIC ALL) 基础设施仓库和 Terraform 模块仓库合并到单个 Terraform 模块仓库功能引入于极狐GitLab 15.1…...

观测云:安全、可信赖的监控观测云服务
引言 近日,“TikTok 遭欧盟隐私监管机构调查并处以 5.3 亿欧元”一案,再次引发行业内对数据合规等话题的热议。据了解,仅 2023 年一年就产生了超过 20 亿美元的 GDPR 罚单。这凸显了在全球化背景下,企业在数据隐私保护方面所面临…...
【python】使用Python和BERT进行文本摘要:从数据预处理到模型训练与生成
《Python OpenCV从菜鸟到高手》带你进入图像处理与计算机视觉的大门! 解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界 随着信息爆炸时代的到来,海量文本数据的高效处理与理解成为亟待解决的问题。文本摘要作为自然语言处理(NLP)中的关键任务,旨在自动生成…...

【PostgreSQL数据分析实战:从数据清洗到可视化全流程】5.3 相关性分析(PEARSON/SPEARMAN相关系数)
👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 5.3 相关性分析(PEARSON/SPEARMAN相关系数)5.3.1 相关性分析理论基础5.3.1.1 相关系数定义与分类5.3.1.2 Pearson相关系数( Pearson Corr…...
Redis面试 实战贴 后面持续更新链接
redis是使用C语言写的。 面试问题列表: Redis支持哪些数据类型?各适用于什么场景? Redis为什么采用单线程模型?优势与瓶颈是什么? RDB和AOF持久化的区别?如何选择?混合持久化如何实现&#x…...
小程序滚动条隐藏(uniapp版本)
单独指定页面隐藏(找到对应的scroll-view) <style> /* 全局隐藏滚动条样式 */ ::-webkit-scrollbar { display: none; width: 0; height: 0; color: transparent; background: transparent; } /* 确保scroll-view组件也隐藏滚动条 */ …...

python基础:序列和索引-->Python的特殊属性
一.序列和索引 1.1 用索引检索字符串中的元素 # 正向递增 shelloworld for i in range (0,len(s)):# i是索引print(i,s[i],end\t\t) print(\n--------------------------) # 反向递减 for i in range (-10,0):print(i,s[i],end\t\t)print(\n--------------------------) print(…...

java反射(2)
package 反射;import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Arrays;public class demo {public static void main(String[] args) throws Exception {// 通过类的全限定名获取对应的 Class 对象…...
C++核心概念全解析:从析构函数到运算符重载的深度指南
目录 前言一、构析函数1.1 概念1.2 语法格式 1.3 核心特性1.4 调用时机1.5 构造函数 vs 析构函数1.6 代码示例 二、this关键字2.1 基本概念2.2 核心特性2.3 使用场景2.3.1 区分成员与局部变量2.3.2 返回对象自身(链式调用)2.3.3 成员函数间传递当前对象2…...
如何巧妙解决 Too many connections 报错?
1. 背景 在日常的 MySQL 运维中,难免会出现参数设置不合理,导致 MySQL 在使用过程中出现各种各样的问题。 今天,我们就来讲解一下 MySQL 运维中一种常见的问题:最大连接数设置不合理,一旦到了业务高峰期就会出现连接…...

自由学习记录(58)
Why you were able to complete the SpringBoot MyBatisPlus task smoothly: Clear logic flow: Database → Entity → Service → Controller → API → JSON response. Errors are explicit, results are verifiable — you know what’s broken and what’s fixed. Sta…...
ES6 知识点整理
一、变量声明:var、let、const 的区别 作用域 var:函数作用域(函数内有效)。let/const:块级作用域({} 内有效,如 if、for)。 变量提升 var 会提升变量到作用域顶部(值为…...

《MATLAB实战训练营:从入门到工业级应用》高阶挑战篇-《5G通信速成:MATLAB毫米波信道建模仿真指南》
《MATLAB实战训练营:从入门到工业级应用》高阶挑战篇-5G通信速成:MATLAB毫米波信道建模仿真指南 🚀📡 大家好!今天我将带大家进入5G通信的奇妙世界,我们一起探索5G通信中最激动人心的部分之一——毫米波信…...

工程师 - 汽车分类
欧洲和中国按字母对汽车分类: **轴距**:简单来说,就是前轮中心点到后轮中心点之间的距离,也就是前轮轴和后轮轴之间的长度。根据轴距的大小,国际上通常把轿车分为以下几类(德国大众汽车习惯用A\B\C\D分类&a…...

57.[前端开发-前端工程化]Day04-webpack插件模式-搭建本地服务器
Webpack常见的插件和模式 1 认识插件Plugin 认识Plugin 2 CleanWebpackPlugin CleanWebpackPlugin 3 HtmlWebpackPlugin HtmlWebpackPlugin 生成index.html分析 自定义HTML模板 自定义模板数据填充 4 DefinePlugin DefinePlugin的介绍 DefinePlugin的使用 5 mode模式配置…...

K8S - 金丝雀发布实战 - Argo Rollouts 流量控制解析
一、金丝雀发布概述 1.1 什么是金丝雀发布? 金丝雀发布(Canary Release)是一种渐进式部署策略,通过逐步将生产流量从旧版本迁移至新版本,结合实时指标验证,在最小化风险的前提下完成版本迭代。其核心逻辑…...

Qt中数据结构使用自定义类————附带详细示例
文章目录 C对数据结构使用自定义类1 QMap使用自定义类1.1 使用自定义类做key1.2 使用自定义类做value 2 QSet使用自定义类 参考 C对数据结构使用自定义类 1 QMap使用自定义类 1.1 使用自定义类做key QMap<key,value>中数据存入时会对存入key值的数据进行比较ÿ…...

数据可视化与分析
数据可视化的目的是为了数据分析,而非仅仅是数据的图形化展示。 项目介绍 项目案例为电商双11美妆数据分析,分析品牌销售量、性价比等。 数据集包括更新日期、ID、title、品牌名、克数容量、价格、销售数量、评论数量、店名等信息。 1、数据初步了解…...
基于大模型预测的产钳助产分娩全方位研究报告
目录 一、引言 1.1 研究背景与意义 1.2 研究目的与方法 二、产钳助产分娩概述 2.1 产钳助产定义与历史 2.2 适用情况与临床意义 三、大模型预测原理与数据基础 3.1 大模型技术原理 3.2 数据收集与处理 3.3 模型训练与验证 四、术前预测与准备 4.1 大模型术前风险预…...