前言:断断续续一直在研究怎样更快更好地完成卡通风格渲染的制作流程,看到《原神》中满满的卡渲细节处理和制作技巧,更是想深入探索其背后的制作流程和实现原理。由于笔者的认知程度和专业水平有限,几乎所有的内容都是自己根据游戏中实际的观察摸索来研究,同时参考了前辈们的很多文章和视频教程。文章中难免会出现很多漏洞、错误,还有很多问题会有已有的更优解决方案,还请各位前辈们多多指正。
很久没有更新过文章了。这段时间遇到了难产的瓶颈期。想用虚幻引擎挑战更高难度的效果,尝试了天云峠雷暴、渊下宫入口、自动检测边缘并交互的瀑布和喷泉等效果,但是都由于各种技术原因(复杂模型的半透排序等),只完成了部分效果,所以一直没有完完整整写出文章。这几个月也一直在断断续续地学习HLSL,我想不如换个方式挑战一下自己,尝试自己独立用Unity的URP管线去手写一些HLSL的Shader,结合我在学习HLSL中的一些方法,与大家共同分享。这是第一次,尝试写一个原神中传送锚点的全息效果。
1.效果分析
还是先上图片:
这是一个通用的Shader,可以看到无论是尘歌壶中的『外景锚点』还是大世界中的『口袋锚点』,他们基本的样子是一致的,所以Shader就应该保持其普适性。特殊的部分则通过特效部分来加以区分(例如上右图口袋锚点中闪闪发光的小星星)。
从模型和效果观察,整个传送锚点分为两部分:锚点主体(中心)和光束(边缘的环绕面片)
①、锚点主体:半透明,边缘菲涅尔,流动的菲涅尔效果
②、光束:半透明,边缘柔滑(边缘菲涅尔),上升的粒子斑点,光束和斑点自下而上渐弱
下面就来单独实现这两部分
2、锚点主体
2.1 建模
全息的传送锚点不好看出其模型的完整结构,但是游戏中还有一个相同的模型:
通过这个参考,快速地就能在Maya中建出来模型
这个模型的UV不需要进行特殊处理,在后面的Shader中不会用到。
2.2 ASE效果还原
在写Shader之前,我先用ASE的节点连了一遍,整理了一下思路。在这里我的做法是尽可能不使用封装好的材质函数(例如Fresnel),而是用最基本的能拿到的节点去连出这些算法,这样在后面用HLSL写Shader的时候会更加清晰。
①、菲涅尔效果
用ViewDirection和模型的WorldNormal去做点乘实现边缘的菲涅尔效果,加入Power参数来控制强度
②、菲涅尔流动效果
这里通过采样一张不断偏移的噪波贴图,将得到的数据与刚刚的菲涅尔效果相乘完成制作
在这里由于菲涅尔效果与模型UV无关,所以我提取出世界位置的X和Y作为一个新的UV去采样这张噪波贴图,解决了菲涅尔区域对这张贴图的采样问题。另外,关于UV的Tilling和Offset没有用封装的函数去做,把所有的算法写开,方便在后面手写Shader的时候理顺思路。
用SubstanceDesigner简单地制作一张合适的贴图:
③、合并
最后将刚刚的两部分合并起来,作为半透明通道,再加入HDR的颜色
实现这个简单的效果
2.3 用HLSL重写Shader
ASE的节点连连看是一个从前到后的过程,我们大部分时间精力都在实现结果上,而不会去考虑准备问题,也就相当于片元着色阶段。在ASE中我们可以直接拿到各种想要的空间信息、函数等,然后通过从前往后线性的思维,直接连出想要的效果。
与ASE不同的是,手写Shader是一个部分逆向的思维:
“我要实现什么效果?我要如何实现这些效果?”这是在片元着色器中要考虑的问题,是节点连连看中的工作重心,也是我们在还原效果中最先会想到的。
“为此我应该做什么准备?”等涉及到的例如空间变换的问题,这是在顶点着色器及顶点着色器到片元着色器的过程中要考虑的。
而Shader的执行顺序是先顶点着色器→片元着色器,也就是很多想拿到的数据可能需要提前做好一系列的变换工作。
因此我们要实现一个思想过程上的转变。在实现效果的时候去思考这样几个问题:“这些数据能不能直接拿到?需不需要做空间变换?需不需要经过顶点着色的过程?”
在用ASE做完连连看以后,我们还可以将其直接编译出代码形式,虽然有些乱,但是部分内容也可以作为之后的参考
下面开始用HLSL写这个Shader效果:
首先是第一步,各种属性的定义,把ASE中定义为参数属性的变量全部写上(这一部分可以直接在ASE编译的代码中粘过来)
Properties
{
[HDR]_Color("Color", Color) = (1,0,0,0)
_NoiseTex("NoiseTex", 2D) = "white" {}
_WSuvTilling("WSuvTilling", Float) = 1
_Speed("Speed", Float) = 1
_TillingY("TillingY", Float) = 1
_TillingX("TillingX", Float) = 1
_NoisePower("MaskPower", Float) = 1
_FresnelPower("FresnelPower", Float) = 1
}
下面开始写SubShader中的内容
由于是半透明,所以RenderType和Queue一定要选择半透明所对应的
Tags
{ "RenderPipeline"="UniversalPipeline"
"RenderType"="Transparent"
"Queue"="Transparent"
}
然后开始写这个Pass
代码头的各种标记:
Name "Forward"
Tags { "LightMode"="UniversalForward" }
Cull Back
Blend SrcAlpha OneMinusSrcAlpha, One OneMinusSrcAlpha
ZWrite Off
ZTest LEqual
Offset 0 , 0
ColorMask RGBA
声明顶点着色器和片元着色器的名字
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
引用各个库的内容
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
在CBUFF中重新声明一开始定义的属性参数变量
CBUFFER_START(UnityPerMaterial)
float4 _Color;
float _WSuvTilling;
float _Speed;
float _TillingX;
float _TillingY;
float _NoisePower;
float _FresnelPower;
CBUFFER_END
定义主纹理和采样器
TEXTURE2D(_NoiseTex);
SAMPLER(sampler_NoiseTex);
在这个Shader效果中,由于顶点着色器中的顶点信息是ObjectPosition下的,所以不能够直接拿到的部分是WorldPosition和WorldNormal,所以在定义Attributes和Varyings结构体中就要加入相关内容,并且在顶点着色器中将这些需要的内容转换完成:
struct Attributes
{
float4 positionOS : POSITION;
float3 normal : NORMAL;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float3 positionWS : TEXCOORD1;
float3 normalWS : TEXCOORD2;
};
Varyings vert (Attributes v)
{
Varyings o = (Varyings)0;
o.positionCS = TransformObjectToHClip(v.positionOS.xyz);
o.positionWS = TransformObjectToWorld(v.positionOS.xyz);
o.normalWS = TransformObjectToWorldNormal(v.normal);
return o;
}
下面就是在片元着色器中复原刚刚ASE连连看中的内容,可以直接对照着自己的ASE连连看把过程写下来了。
菲涅尔效果:
//Fresnel
half3 worldPosition = i.positionWS;
half3 viewDirectionWS = normalize(GetCameraPositionWS() - i.positionWS);
half fresnel = pow(1 - saturate(dot(viewDirectionWS, i.normalWS)), _FresnelPower);
菲涅尔部分的噪波流动效果:
//WorldPosition UV Tilling And Offset
half2 worldPosUV = (half2(worldPosition.x, worldPosition.y)) / _WSuvTilling;
half2 texTilling = (half2(_TillingX, _TillingY));
half2 texSpeed = (half2(0, _Speed * _TimeParameters.x));
half2 texUV = worldPosUV * texTilling + texSpeed;
//Sample Texture
half4 mainTex = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, texUV);
合并输出
//Alpha
half alpha = (1 - saturate(pow(saturate(mainTex.x), _NoisePower))) * fresnel;
return half4(_Color.xyz, alpha);
整个过程就结束了。
最终的效果:
3.光束
3.1 建模
光束用一个去掉上下底面的圆柱代替即可,注意UV要完全展开充满整个第一象限,顶部底部应与模型方向保持一致
3.2 ASE效果还原
实现方式与上面的锚点Shader类似
①、光束柔软效果
这里为了实现光束边缘柔软的效果,同样使用菲涅尔的操作对边缘进行柔滑。
由于自下而上光束强度逐渐减弱,因此通过拿到UV的V方向作为遮罩去控制渐弱程度。
②、星星斑点粒子上升效果
单独对模型UV的V方向做一个持续的偏移,采样一张星星点点的贴图,实现效果
同样星星斑点自下而上强度逐渐减弱,因此通过拿到UV的V方向作为遮罩去控制渐弱程度
③、合并
最后将两部分相加,作为半透明通道去使用,再单独连一个HDR颜色完成制作
3.3 用HLSL重写Shader
准备工作与锚点主体的相同,但是要在顶点着色器中加入
float2 uv :TEXCOORD0
也就是UV的变换,不然的话在片元着色器里拿不到模型的UV。
然后对应刚刚ASE连连看的过程,还原这个效果
half4 frag (Varyings i) : SV_Target
{
//1.Surface
half3 viewDirectionWS = normalize(GetCameraPositionWS() - i.positionWS);
half surfaceFresnel = pow(saturate(dot(viewDirectionWS, i.normalWS)), _SurFrePower) * _SurFreIntensity;
half2 texUV = i.uv;
half surfaceUVMaskY = pow(saturate((1- (half(i.uv.y)))), _SurTopMaskPower);
half surfaceAlpha = surfaceFresnel * surfaceUVMaskY;
//2.Star
half2 starTilling = (half2(_StarTillingX, _StarTillingY));
half2 starSpeed = (half2(0, _StarSpeed * _TimeParameters.x));
half2 starUV = i.uv * starTilling + starSpeed;
half4 starTex = SAMPLE_TEXTURE2D(_StarTex, sampler_StarTex, starUV);
half starUVMaskY = pow(saturate((1- (half(i.uv.y)))), _StarTopMaskPower);
half starAlpha = starTex.x * starUVMaskY;
//3.Alpha and Return
half Alpha = saturate(surfaceAlpha + starAlpha);
return half4(_Color.xyz, Alpha);
}
最终效果:
这是第一次写关于用Unity的URP管线手写HLSL Shader的分享,代码可能不是最优最简洁的。整个的制作过程也是我去思考手写和连连看区别的探索过程,希望能把这些自己的方法和想法分享出来,帮助到同样刚刚入门手写Shader的同学,与行业的前辈大佬们共同交流。
道阻且长,行则将至。希望各位前辈大佬们能够给予评价和指导!
最后的最后,春招已经开始了,祝愿大家都能够心想事成,好运连连,offer连连!
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:704559159@qq.com