📍

选择城市

立即咨询
精选文章 / 深度解读

Unity技术美术教程,可交互的能量盾,附源文件

演示效果https://www.zhihu.com/video/1626255034097811456搭建工程新建一个工程,切换至URP渲染管线,我使用的是Unity2022.2,其他版本也是可以的记得勾选DepthTexture和OpaqueTexture,之后会用到新建Sha...

演示效果



https://www.zhihu.com/video/1626255034097811456

搭建工程

新建一个工程,切换至URP渲染管线,我使用的是Unity2022.2,其他版本也是可以的



记得勾选DepthTexture和OpaqueTexture,之后会用到



新建Shader





在顶点着色器中输出UV,法线等信息,用于后续计算

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};

struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD1;
float3 normal : TEXCOORD2;
float3 worldPos : TEXCOORD3;
float4 screenPos : TEXCOORD4;
float4 localPos : TEXCOORD5;
};

v2f vert (appdata v)
{
v2f o;
o.vertex = TransformObjectToHClip(v.vertex.xyz);
o.uv = v.uv;
o.normal = TransformObjectToWorldNormal(v.normal);
o.worldPos = TransformObjectToWorld(v.vertex.xyz);
o.localPos = v.vertex;
o.screenPos = o.vertex;
#if UNITY_UV_STARTS_AT_TOP
o.screenPos.y *= -1;
#endif
return o;
}

边缘光

先添加边缘光,用模型法线和观察方向做点乘

//Properties
_RimPower ("RimPower", Float) = 1
[HDR] _RimColor ("RimColor", Color) = (1, 1, 1, 1)

float _RimPower;
float4 _RimColor;

//frag
float3 normal = normalize(i.normal);
float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
float ndv = dot(normal, viewDir);
if(ndv < 0) {
ndv = abs(ndv);
}
ndv = 1 - ndv;

float rimIntensity = pow(ndv, _RimPower);
finalColor += _RimColor * rimIntensity;
finalColor.a = saturate(finalColor.a);


好像缺了点什么,噢,忘记开Bloom了......



接触高亮

能量盾和其他物体接触时,需要有一个亮边,用盾像素点的屏幕坐标采样场景深度图,再用场景深度和盾像素点深度做对比,当两者足够接近时,显示亮边

//Properties
_IntersectionWidth ("IntersectionWidth", Float) = 1
[HDR] _IntersectionColor ("IntersectionColor", Color) = (1, 1, 1, 1)

float _IntersectionWidth;
float4 _IntersectionColor;

sampler2D _CameraDepthTexture;

//frag
i.screenPos.xyz /= i.screenPos.w;
float2 screenUV = i.screenPos.xy;
screenUV = (screenUV + 1) / 2;

float selfZ = i.screenPos.z;
float sceneZ = tex2D(_CameraDepthTexture, screenUV).r;
float linearSelfZ = LinearEyeDepth(selfZ, _ZBufferParams);
float linearSceneZ = LinearEyeDepth(sceneZ, _ZBufferParams);
float zDifference = linearSceneZ - linearSelfZ;
if(zDifference < _IntersectionWidth) {
float intersectionIntensity = (1 - zDifference / _IntersectionWidth);
intersectionIntensity = saturate(intersectionIntensity);
intersectionIntensity = pow(intersectionIntensity, 4);
finalColor += _IntersectionColor * intersectionIntensity;
finalColor.a = saturate(finalColor.a);
}


纹理

接下来给能量盾添加纹理,球的UV是不均匀的,如果直接用UV采样,贴图在顶部会被压缩,在中间区域会被拉伸。

这里我把2D贴图合成了Cubemap,用法线采样,并且判断了当前像素点是否是背面,如果是,则不显示纹理



//Properties
_PatternTex ("PatternTex", Cube) = "white" {}
_PatternPower ("PatternPower", Float) = 1
[HDR] _PatternColor ("PatternColor", Color) = (1, 1, 1, 1)

samplerCUBE _PatternTex;
float _PatternPower;
float4 _PatternColor;

//frag
int isFrontFace = 1;
//......
if(ndv < 0) {
isFrontFace = 0;
}

float patternIntensity = texCUBE(_PatternTex, normal).a * isFrontFace;
patternIntensity *= pow(ndv, _PatternPower);
finalColor += patternIntensity * _PatternColor;
finalColor.a = saturate(finalColor.a);


接下来给纹理添加流动效果,用一张网格遮罩和纹理做叠加



//Properties
_Mask ("Mask", 2D) = "black" {}
[HDR] _MaskColor ("MaskColor", Color) = (1, 1, 1, 1)

sampler2D _Mask;
float4 _Mask_ST;
float4 _MaskColor;

//frag
float mask = 0;
mask += tex2D(_Mask, i.uv * _Mask_ST.xx + _Mask_ST.zz * _Time.y).a;
mask += tex2D(_Mask, i.uv * _Mask_ST.yy + _Mask_ST.ww * _Time.y).a;
mask = saturate(mask);
finalColor += patternIntensity * mask * _MaskColor;
finalColor.a = saturate(finalColor.a);


https://www.zhihu.com/video/1626235816711299073


溶解

接下来制作启动能量盾的溶解效果,用像素点的y坐标控制溶解,再叠加噪声做不规则轮廓

//Properties
_Noise ("Noise", 2D) = "white" {}

_DissolveThreshold ("DissolveThreshold", Float) = 1
_DissolveWidth ("DissolveWidth", Float) = 0.1
[HDR] _DissolveColor ("DissolveColor", Color) = (1, 1, 1, 1)

sampler2D _Noise;
float4 _Noise_ST;

float _DissolveThreshold;
float _DissolveWidth;
float4 _DissolveColor;

//frag
if(i.localPos.y > _DissolveThreshold) {
discard;
}
else if(i.localPos.y > _DissolveThreshold - _DissolveWidth) {
float t = (i.localPos.y - _DissolveThreshold + _DissolveWidth) / _DissolveWidth;
float noise = tex2D(_Noise, i.uv * _Noise_ST.xy + _Noise_ST.zw * _Time.y);
noise = lerp(1, noise * (1 - t), pow(t, 0.5));
if(noise > 0.5) {
finalColor = _DissolveColor;
}else {
discard;
}
}


https://www.zhihu.com/video/1626236203392466944

颜色交互

接下来制作最复杂的交互功能,思路是用脚本把交互点,交互半径,交互颜色传入材质球,然后在shader中,计算像素点和交互点的距离,如果在交互半径内则显示交互颜色。

听起来很简单,就是一句话的事,实现起来......



新建一个Shield.cs脚本,把传递交互信息的API封装好

public class Shield : MonoBehaviour {
private class InteractionData {
public Color color;
public Vector3 interactionStartPos;
public float timer;
}

public List<Material> materials;

private List<InteractionData> interactionDatas = new List<InteractionData>();

private void Update() {
//......
for (int i = 0; i < materials.Count; i++) {
materials[i].SetInt("_InteractionNumber", interactionDatas.Count);

if (interactionDatas.Count > 0) {
materials[i].SetVectorArray("_InteractionStartPosArray", interactionStartPosArray);
materials[i].SetFloatArray("_InteractionInnerRadiusArray", interactionInnerRadiusArray);
materials[i].SetFloatArray("_InteractionOuterRadiusArray", interactionOuterRadiusArray);
materials[i].SetFloatArray("_InteractionAlphaArray", interactionAlphaArray);
materials[i].SetColorArray("_InteractionColorArray", interactionColorArray);
materials[i].SetFloatArray("_DistortAlphaArray", distortAlphaArray);
}
}
}

public void AddInteractionData(Vector3 pos, Color color) {
if (interactionDatas.Count >= 100) {
return;
}

InteractionData interactionData = new InteractionData();
interactionData.color = color;
interactionData.interactionStartPos = pos;
interactionDatas.Add(interactionData);
}
}

新建一个ShootManager.cs脚本,在点击鼠标时,做射线检测,如果碰撞到能量盾,则调用交互接口

public class ShootManager : MonoBehaviour {
[ColorUsage(true, true)] public Color interactionColor;

private void Update() {
if (Input.GetMouseButtonDown(0)) {
RaycastHit hitInfo;
bool hited = Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out hitInfo, Mathf.Infinity);
if (hited) {
Shield.instance.AddInteractionData(hitInfo.point, interactionColor);
}
}
}
}

然后在Shader里计算

//Properties
int _InteractionNumber;
float3 _InteractionStartPosArray[100];
float _InteractionInnerRadiusArray[100];
float _InteractionOuterRadiusArray[100];
float _InteractionAlphaArray[100];
float4 _InteractionColorArray[100];

float _DistortAlphaArray[100];

float GetInteractionIntensity(v2f i, float3 startPos, float innerRadius, float outerRadius) {
float dist = distance(i.worldPos, startPos);
if(dist > outerRadius || dist < innerRadius) {
return 0;
}
else {
float intensity = (dist - innerRadius) / (outerRadius - innerRadius);
return intensity;
}
}

//frag
float interactionIntensity = 0;
float4 interactionColor = 0;
for(int iii = 0; iii < _InteractionNumber; iii++) {
float tempInteractionIntensity = GetInteractionIntensity(i, _InteractionStartPosArray[iii], _InteractionInnerRadiusArray[iii], _InteractionOuterRadiusArray[iii]) * _InteractionAlphaArray[iii];
interactionIntensity += tempInteractionIntensity;

interactionColor += _InteractionColorArray[iii] * tempInteractionIntensity;
}

interactionIntensity = saturate(interactionIntensity);

finalColor += interactionColor;
finalColor.a = saturate(finalColor.a);


https://www.zhihu.com/video/1626237758866972672

在交互区域,对纹理做提亮和扭曲

//Properties
_DistortNormal ("DistortNormal", 2D) = "bump" {}
_DistortIntensity ("DistortIntensity", Float) = 1

sampler2D _DistortNormal;
float4 _DistortNormal_ST;
float _DistortIntensity;

float GetDistortIntensity(v2f i, float3 startPos, float innerRadius, float outerRadius) {
float dist = distance(i.worldPos, startPos);
if(dist > outerRadius) {
return 0;
}
else {
float intensity = dist / outerRadius;
return intensity;
}
}

//frag
float3 distortNormal = UnpackNormal(tex2D(_DistortNormal, i.uv * _DistortNormal_ST.xy + _DistortNormal_ST.zw * _Time.y));
distortNormal *= _DistortIntensity * distortIntensity;

float distortIntensity = 0;
for(int iii = 0; iii < _InteractionNumber; iii++) {
//......
distortIntensity += GetDistortIntensity(i, _InteractionStartPosArray[iii], _InteractionInnerRadiusArray[iii], _InteractionOuterRadiusArray[iii]) * _DistortAlphaArray[iii];
distortIntensity = saturate(distortIntensity);
}

float patternIntensity = texCUBE(_PatternTex, normal + distortNormal).a * isFrontFace;
patternIntensity *= pow(ndv + interactionIntensity, _PatternPower);


https://www.zhihu.com/video/1626213140663791616

扭曲交互

交互功能还差最后一步



接下来做交互区域的屏幕扭曲效果,原理是用一张法线贴图修改屏幕UV,然后采样_CameraOpaqueTexture,

新建Shield_Distort.shader,把能量盾Shader里的代码复制过来,添加

sampler2D _CameraOpaqueTexture;
float4 _CameraOpaqueTexture_TexelSize;

修改片元着色器

float4 frag (v2f i) : SV_Target
{
float4 finalColor = 0;

float distortIntensity = 0;
for(int iii = 0; iii < _InteractionNumber; iii++) {
distortIntensity += GetDistortIntensity(i, _InteractionStartPosArray[iii], _InteractionInnerRadiusArray[iii], _InteractionOuterRadiusArray[iii]) * _DistortAlphaArray[iii];
distortIntensity = saturate(distortIntensity);
}

float3 distortNormal = UnpackNormal(tex2D(_DistortNormal, i.uv * _DistortNormal_ST.xy + _DistortNormal_ST.zw * _Time.y));
distortNormal *= _DistortIntensity * distortIntensity;

i.screenPos.xyz /= i.screenPos.w;
float2 screenUV = i.screenPos.xy;
screenUV = (screenUV + 1) / 2;

finalColor = tex2D(_CameraOpaqueTexture, screenUV + distortNormal.xy * _CameraOpaqueTexture_TexelSize.xy);

return finalColor;
}

把能量盾复制一份,换上新材质





https://www.zhihu.com/video/1626217980341170177

至此,我们的能量盾就完成了!



源文件下载

Github https://github.com/MagicStones23/Unity-Shader-Tutorial-Interactable-Energy-Shield

百度网盘 https://pan.baidu.com/s/1pyjMz4aokWlqRluV86Wq1w?pwd=1111 提取码:1111

备注:工程中部分素材取自互联网,请勿在商业中使用

B站主页

阅读重点

这篇内容能帮你快速理解什么

帮助快速理解主题

通过更完整的主题说明和结构表达,帮助用户更快抓住重点,也让搜索系统更容易识别页面主题。

帮助判断下一步动作

让访问者快速理解当前问题、可行方法以及下一步应该继续看案例、看服务还是直接沟通。

帮助继续浏览与沟通

文章页不只是获取流量,也承担继续阅读、查看服务和发起咨询的承接作用。

继续了解

继续了解这个主题前,你可能还关心这些问题

为什么这类主题适合写成文章?

因为很多用户会通过问题词、对比词和方案词进入网站,文章页越清楚,越容易覆盖更具体的需求。

为什么文章页不能只有正文?

仅有正文不利于继续浏览和转化,文章页还需要总结、问答、相关推荐与咨询入口来承接用户。

看完之后下一步可以做什么?

可以继续看同类文章、服务页与案例页,也可以直接沟通官网升级与搜索优化需求。

这篇文章能帮助我解决什么具体问题?

这篇文章围绕当前主题提供了详细的解决方案、操作步骤和注意事项,帮助你快速理解核心要点并应用到实际场景中。

如何判断这篇文章的内容是否权威可靠?

内容基于实际项目经验和技术实践编写,结合行业标准和最佳实践,同时提供案例数据和方法论支撑,确保专业性和可操作性。

这类内容对SEO和网站排名有什么帮助?

优质的长文内容和FAQ结构能够提升页面主题相关性、增加用户停留时间、降低跳出率,这些都有助于搜索引擎评估页面质量并提升排名表现。

AI搜索引擎会如何理解和引用这类内容?

AI搜索系统会提取文章的实体信息、观点结论和结构化问答,当用户提出相关问题时,可能会引用本文作为答案来源或参考依据。

如果我有更多相关问题可以咨询谁?

可以通过页面底部的联系方式直接咨询我们的专业团队,包括电话、QQ或在线表单,我们会根据你的具体情况提供针对性的建议和方案。

这篇文章和同类内容有什么不同之处?

本文不仅提供理论知识,还包含实战经验、避坑指南和可执行的行动建议,同时兼顾传统SEO和新兴的GEO生成式搜索优化视角。

多久需要更新一次这类内容以保持时效性?

建议每季度审查并更新一次关键数据和案例,如果涉及技术工具或算法变化则需要更频繁地维护,确保内容持续为用户提供准确价值。