【Unity URP】卡通风格水晶Shader

Posted by FlowingCrescent on 2021-06-09
Estimated Reading Time 9 Minutes
Words 1.7k In Total
Viewed Times

最近看到一个比较有趣的水晶效果:
image.png
出自:https://github.com/CJT-Jackton/URP-Anime-Crystal-Shader

感觉效果非常不错便下载过来看了下原理,作者用的是Shader Graph,我也顺便改写成了代码版本

原理

最核心的内容,便是表面那种三角形纹理如何生成以及移动

三角形的生成

三角形的生成,用的是鼎鼎大名的德劳内三角化
可参考以下资料:
Wiki百科
平面三角形分割 - 德劳内三角化

说白了,就是在平面中生成三角形且避免有“较瘦”的三角形的算法,
作者直接参考(Copy)了ShaderToy中的一个实现:
https://www.shadertoy.com/view/4sKyRD

他引用了名为DelaunayTriangulationNoise.hlsl的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#ifndef DELAUNAY_TRIANGULATION_NOISE_INCLUDED
#define DELAUNAY_TRIANGULATION_NOISE_INCLUDED

float2 delaunay_noise_randomVector(float2 UV, float offset)
{
float2x2 m = float2x2(15.27, 47.63, 99.41, 89.98);
UV = frac(sin(mul(UV, m)) * 46839.32);
return float2(sin(UV.y * offset), cos(UV.x * offset)) * 0.25;
}

// Signed distance to a line crossing (p0, p1) segment.
float distLine(float2 p0, float2 p1)
{
float2 e0 = p1 - p0;
return dot(p0, normalize(float2(e0.y, -e0.x)));
}

// Use "Parabolic lifting" method to calculate if two triangles are about to flip.
// This is actually more reliable than circumscribed circle method.
// The technique is based on duality between Delaunay Triangulation
// and Convex Hull, where DT is just a boundary of convex hull
// of projected seeds onto paraboloid.
// We project (h1 h2 h3) triangle ontot paraboloid
// and return the distance of the origin
// to a plane crossing projected triangle.
float flipDistance(float2 h1, float2 h2, float2 h3)
{
// Project triangle on paraboloid.
float3 g1 = float3(h1.x, h1.y, dot(h1, h1));
float3 g2 = float3(h2.x, h2.y, dot(h2, h2));
float3 g3 = float3(h3.x, h3.y, dot(h3, h3));

return dot(g1, cross(g3 - g1, g2 - g1));
}

// Find the distance to the closest Delaunay edge in quad (h0, h1, h2, h3).
void delaunayQuad(float2 h0, float2 h1, float2 h2, float2 h3, float2 pos0, float2 pos1, float2 pos2, float2 pos3, inout float distance, inout float2 center)
{
// Get the distance to quad edges.
// note: in general it can be concave, but we don't care.
float minDistance = min(min(distLine(h0, h1), distLine(h1, h2)), min(distLine(h2, h3), distLine(h3, h0)));

if (minDistance < 0.0) return; // Outside the quad

// Calculate flip distance relative to h2.
float dc = flipDistance(h0 - h2, h1 - h2, h3 - h2);

// Flipping rotates diagonal from (h0 h2) to (h3 h1).
float2 diagonalPosition0 = (dc > 0.0) ? h3 : h0;
float2 diagonalPosition1 = (dc > 0.0) ? h1 : h2;

// Calculate distance to diagonal.
float distToDiagonal = distLine(diagonalPosition0, diagonalPosition1);

// Calculate distance to diagonal (positive from both sides).
minDistance = min(minDistance, abs(distToDiagonal));

distance = min(minDistance, distance);

if (dc > 0.0)
{
if (distToDiagonal > 0.0)
center = (pos1 + pos2 + pos3) / 3.0;
else
center = (pos0 + pos1 + pos3) / 3.0;
}
else
{
if (distToDiagonal > 0.0)
center = (pos0 + pos2 + pos3) / 3.0;
else
center = (pos0 + pos1 + pos2) / 3.0;
}
}

// Final function visits 4 quads around the center cell.
void delaunayTriangulation(float2 UV, float AngleOffset, out float minDistance, out float2 center)
{
float2 grid = floor(UV);

// Cache sites coordinate.
float2 site[9];
float2 h[9];

// Iterate sites.
for (int y = -1; y <= 1; y++)
{
for (int x = -1; x <= 1; x++)
{
float2 lattice = float2(x, y);
float2 offset = float2(0.5, 0.5) + delaunay_noise_randomVector(grid + lattice, AngleOffset);
site[(x + 1) + (y + 1) * 3] = grid + lattice + offset;
h[(x + 1) + (y + 1) * 3] = site[(x + 1) + (y + 1) * 3] - UV;
}
}

minDistance = 8.0;
center = float2(0.0, 0.0);

delaunayQuad(h[3], h[0], h[1], h[4], site[3], site[0], site[1], site[4], minDistance, center);
delaunayQuad(h[4], h[1], h[2], h[5], site[4], site[1], site[2], site[5], minDistance, center);
delaunayQuad(h[7], h[4], h[5], h[8], site[7], site[4], site[5], site[8], minDistance, center);
delaunayQuad(h[6], h[3], h[4], h[7], site[6], site[3], site[4], site[7], minDistance, center);
}

// Delaunay Triangluation
// https://www.shadertoy.com/view/4sKyRD
void Unity_Delaunay_Triangulation_float(float2 UV, float AngleOffset, float CellDensity, out float Out, out float2 Cells)
{
delaunayTriangulation(UV * CellDensity, AngleOffset, Out, Cells);
}

#endif // DELAUNAY_TRIANGULATION_NOISE_INCLUDED

当然,不是那么容易能够看懂的,不过我们只是使用的话也没必要太深究
使用Unity_Delaunay_Triangulation_float()函数,我们可以获得基于UV值的Out与Cells,
Out似乎是指三角形内的点到对角线的距离,这个值我们也用不到;
Cells则是每个三角形的中心位置,我们便是使用这个值来随机生成亮度不同的三角形。
image.png
作者叠了两个德劳内三角化
不过为什么特地分了两个三角化的Strength还乘在了一起……那么设置两个Strength进行控制便毫无意义了。

三角形的偏移

三角形的奇特偏移造就了这个效果的良好观感,
作者使用了切线空间的“折射向量”来对输入德劳内三角化的uv进行偏移,
因此在观感上,会发现三角形的偏移与其法线以及我们的观察角度有关。
image.png

利用环境光球谐值进行着色

image.png

还原效果

image.png
左边是我写的Shader,右边是原版
image.png

URP Shader代码

在原有基础上增加了接收阴影

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
Shader "Custom/Crystal"
{
Properties
{
_BaseColor ("Crystal Colour", Color) = (1, 1, 1, 1)
_RefractionIndex ("Index Of Refraction", Range(1, 2)) = 1.7
_AngleOffset1 ("Angle Offset 1", float) = 8
_CellDensity1 ("Cell Density 1", float) = 4
_AngleOffset2 ("Angle Offset 2", float) = 6
_CellDensity2 ("Cell Density 2", float) = 5
_TriangleStrength ("Triangle Strength 1", float) = 2
[Space(10)]
_RimStrenth ("Rim Light Strength", float) = 1
_Roughness ("Roughness", Range(0, 1)) = 0.007
}
SubShader
{
Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalRenderPipeline" }

HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "DelaunayTriangulationNoise.hlsl"

CBUFFER_START(UnityPerMaterial)
float4 _BaseColor;
float _AngleOffset1, _CellDensity1, _AngleOffset2, _CellDensity2, _Roughness;
float _RefractionIndex, _TriangleStrength, _TriangleStrength2, _RimStrenth;
CBUFFER_END
ENDHLSL

Pass
{
Name "Crystal"
Tags { "LightMode" = "UniversalForward" }

HLSLPROGRAM

#pragma vertex vert
#pragma fragment frag

#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
#pragma multi_compile _ _SHADOWS_SOFT

struct a2v
{
float4 positionOS: POSITION;
float2 uv: TEXCOORD0;
float4 normal: NORMAL;
float4 tangent: TANGENT;
};

struct v2f
{
float4 positionCS: SV_POSITION;
float2 uv: TEXCOORD0;
float3 positionWS: TEXCOORD1;
float3 normalWS: TEXCOORD2;
float3 tangentWS: TEXCOORD3;
};


v2f vert(a2v v)
{
v2f o;

VertexPositionInputs positionInputs = GetVertexPositionInputs(v.positionOS.xyz);
o.positionCS = positionInputs.positionCS;
o.positionWS = positionInputs.positionWS;

o.uv = v.uv;
VertexNormalInputs vertexNormalInput = GetVertexNormalInputs(normalize(v.normal), v.tangent);
o.normalWS = vertexNormalInput.normalWS;
o.tangentWS = vertexNormalInput.tangentWS;
return o;
}

half3 Highlights(half3 positionWS, half roughness, half3 normalWS, half3 viewDirectionWS)
{
Light mainLight = GetMainLight();
half roughness2 = roughness * roughness;
half3 halfDir = SafeNormalize(mainLight.direction + viewDirectionWS);
half NoH = saturate(dot(normalize(normalWS), halfDir));
half LoH = saturate(dot(mainLight.direction, halfDir));
// GGX Distribution multiplied by combined approximation of Visibility and Fresnel
half d = NoH * NoH * (roughness2 - 1.h) + 1.0001h;
half LoH2 = LoH * LoH;
half specularTerm = roughness2 / ((d * d) * max(0.1, LoH2) * (roughness + 0.5) * 4);
specularTerm = min(specularTerm, 10);
return specularTerm * mainLight.color * mainLight.distanceAttenuation;
}

float GetRandomValue(float2 uv)
{
return frac(sin(dot(uv.xy, float2(18.5348, 43.253))) * 24358.545386);
}

float CalculateFresnel(float3 viewDir, float3 normal)
{
float R_0 = (1 - 1 / _RefractionIndex) / (1 + 1 / _RefractionIndex);
R_0 *= R_0;
return R_0 + (1.0 - R_0) * pow((1.0 - saturate(dot(viewDir, normal))), _RimStrenth);
}

half4 frag(v2f i): SV_Target
{

real3x3 TtoW = CreateTangentToWorld(i.normalWS, i.tangentWS, -1);
float3 viewDirectionWS = SafeNormalize(GetCameraPositionWS() - i.positionWS.xyz);
float3 viewDirectionTS = TransformWorldToTangent(viewDirectionWS, TtoW);
float4 shadowCoord = TransformWorldToShadowCoord(i.positionWS.xyz);
Light light = GetMainLight(shadowCoord);
//-------------DelaunayTriangulationNoise-------------
float3 normal = TransformWorldToTangent(i.normalWS, TtoW);
float3 refractDir = refract(viewDirectionTS, normal, 1 / _RefractionIndex);
float2 refractDir_XY = refractDir.xy / refractDir.z;

float Out1;
float2 Cell1;
float Out2;
float2 Cell2;
Unity_Delaunay_Triangulation_float(i.uv * float2(1.2, 1) + refractDir_XY, _AngleOffset1, _CellDensity1, Out1, Cell1);
Unity_Delaunay_Triangulation_float(i.uv * float2(1, 1.75) + refractDir_XY, _AngleOffset2, _CellDensity2, Out2, Cell2);
float randomTriangle1 = GetRandomValue(Cell1);
float randomTriangle2 = GetRandomValue(Cell2);

//remap shadow value from [0, 1] to [0.5, 1]
float triangleValue = randomTriangle1 * randomTriangle2 * _TriangleStrength * (light.shadowAttenuation * 0.5 + 0.5);

//-------------Rim-------------
float rim = CalculateFresnel(viewDirectionWS, i.normalWS);
rim = saturate(rim);

//-------------SH-------------
half3 SH = SampleSH(float4(-i.normalWS, 1));

//-------------HighLight-------------
float3 highLight = Highlights(i.positionWS, _Roughness, i.normalWS, viewDirectionWS) * light.shadowAttenuation;

return float4((triangleValue + rim) * SH * _BaseColor + highLight, 1);
}
ENDHLSL

}
}
}

美中不足

作者在Github中表示这个Shader只能用于Lowpoly的物体,如果面数过高则效果不好
个人试了试,感觉确实显得有些杂乱
image.png


感谢您阅读完本文,若您认为本文对您有所帮助,可以将其分享给其他人;若您发现文章对您的利益产生侵害,请联系作者进行删除。