Substance Painter - Unity 效果还原的一种方法

美术经常有这样的困扰,“为什么我在SP里的效果是这样的,进了Unity就变成那样了?”

对于美术生产来说,不能所见即所得,进引擎的最终效果靠猜,是非常不利于美术创作的。

另一方面,没有这样的一个与引擎内效果统一的生产环境,也不利于外包的工作。

所以引擎与生产环境效果的统一,是打通PBR Pipeline的重要一环,对工业化水平的提升是非常有意义的。

目前网上找到的分享中,大都有这样的困扰,就是由于Unity和SP两边光照环境的区别,很难做到非常一致的效果。

这里给大家分享一个Unity效果还原到SP的方法,通过将Unity的光照与Shader还原到SP中,基本可以做到两边的效果比较相似。

光照环境

在SP的Shader中直接写入直接光照来统一两边表现可以比较简单的实现,这里按下不表,主要讲一下怎么把Unity的环境高光挪到SP里。

SP中是通过对环境进行重要性采样来计算IBL的,而Unity中是根据粗糙度来采样Cubemap,但是这个Cubemap是Unity提前Bake好的。


Studio 03 的mipmap,在Unity(上)与SP(下)中的对比

挖了下Unity源码,没有看到具体是怎么Bake的,所以使用了非常暴力的做法,直接把Unity烘焙好的环境球提供给SP,Shader直接采样这个Unity的Cubemap,不再采样SP自己的环境。通过这样的方式,来统一两边的环境光照。

Shader

Substance的API文档和自带的几个Shader都是很好的参考对象。

Shader相对比较直接,把Unity的算法搬过来就好,基本在以下几个地方:

Internal-DeferredReflections
UnityPBSLighting
UnityGlobalIllumination
UnityStandardBRDF
UnityImageBasedLighting

可以先把half3这些define了,把常用函数也复制过来,抄起来方便很多,这点可以参考nagnae的这篇blog文章。另外这篇文章的最后也准备了懒人包。

因为是先写了原生Unity 5.6的效果再在那个的基础上还原自己项目的Shader,这里稍微分享下原生Unity SP Shader的片段。

间接高光部分。

1
2
3
4
5
6
7
8
9
10
11
half3 IndirectSpecular(LocalVectors vectors, half roughness, half occlusion)
{
roughness = roughness*(1.7 - 0.7*roughness);

half mip = perceptualRoughnessToMipmapLevel(roughness);
half3 reflUVW = reflect(-vectors.eye, vectors.normal);

half3 env0 = envSampleLODCustom(reflUVW, mip).rgb;

return env0 * occlusion;
}


写Shader的过程中可以通过对比一些中间结果确定自己没跑偏,上图显示的是mip

BRDF(连人家的注释都一起抄过来),这里只有高光。

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
half3 UNITY_BRDF_PBS(LocalVectors vectors, UnityStandardData data, float oneMinusReflectivity)
{
half perceptualRoughness = SmoothnessToPerceptualRoughness (data.smoothness);
half roughness = PerceptualRoughnessToRoughness(perceptualRoughness);

vec3 Ln = half3(0, 1, 0);
vec3 Hn = normalize(Ln + vectors.eye);

float nv = saturate(dot(data.normalWorld, vectors.eye));
float nl = saturate(dot(vectors.normal, Ln));
float nh = saturate(dot(vectors.normal, Hn));

float lv = saturate(dot(Ln, vectors.eye));
float lh = saturate(dot(Ln, Hn));

float vh = saturate(dot(vectors.eye, Hn));

half diffuseTerm = DisneyDiffuse(nv, nl, lh, perceptualRoughness) * nl;

half V = SmithJointGGXVisibilityTerm (nl, nv, roughness);
half D = GGXTerm (nh, roughness);

half specularTerm = V * D * 3.14159265359; // Torrance-Sparrow model, Fresnel is applied later
// specularTerm * nl can be NaN on Metal in some cases, use max() to make sure it's a sane value
specularTerm = max(0, specularTerm * nl);

// To provide true Lambert lighting, we need to be able to kill specular completely.
specularTerm *= any(data.specularColor) ? 1.0 : 0.0;

// surfaceReduction = Int D(NdotH) * NdotH * Id(NdotL>0) dH = 1/(roughness^2+1)
float surfaceReduction = 1.0 / (roughness*roughness + 1.0);

half grazingTerm = saturate(data.smoothness + (1 - oneMinusReflectivity));

return surfaceReduction * IndirectSpecular(vectors, perceptualRoughness, data.occlusion) * FresnelLerp(data.specularColor, vec3(grazingTerm), nv);
}

结果

放一个原生Unity配Substance Painter的效果。

写在最后的一些实用小东西

在SP里刷新Shader

SP的Custom Shader是不会实时更新效果的,如何在SP里刷新修改后的Shader呢?

首先在某个位置创建自己的glsl Shader;

然后将Shader文件拖进SP,在导入设置里选择project项;

修改了Shader文件,想要在SP中看到效果的时候,右键Shader选择Reload;

这个小技巧是参考了harayoki的分享

Unity环境球转到Substance的一些细节

SP不认Cubemap,所以需要做一个转换。

这个Staff推荐的HDRShop我只试了v1,不太好用。推荐使用ImageViewer这个工具,很方便。

懒人包

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
half Pow5 (half x)
{
return x*x * x*x * x;
}

half SpecularStrength(half3 specular)
{
return max (max (specular.r, specular.g), specular.b);
}

half perceptualRoughnessToMipmapLevel(half perceptualRoughness)
{
return perceptualRoughness * MIP_STEP;
}

half DisneyDiffuse(half NdotV, half NdotL, half LdotH, half perceptualRoughness)
{
half fd90 = 0.5 + 2 * LdotH * LdotH * perceptualRoughness;
// Two schlick fresnel term
half lightScatter = (1 + (fd90 - 1) * Pow5(1 - NdotL));
half viewScatter = (1 + (fd90 - 1) * Pow5(1 - NdotV));

return lightScatter * viewScatter;
}

half SmithJointGGXVisibilityTerm (half NdotL, half NdotV, half roughness)
{

half a = roughness;
half lambdaV = NdotL * (NdotV * (1 - a) + a);
half lambdaL = NdotV * (NdotL * (1 - a) + a);

return 0.5f / (lambdaV + lambdaL + 1e-5f);
}

half GGXTerm (half NdotH, half roughness)
{
half a2 = roughness * roughness;
half d = (NdotH * a2 - NdotH) * NdotH + 1.0f; // 2 mad
return 0.31830988618 * a2 / (d * d + 1e-7f); // This function is not intended to be running on Mobile,
// therefore epsilon is smaller than what can be represented by half
}

half3 FresnelLerp(half3 F0, half3 F90, half cosA)
{
half t = Pow5(1 - cosA); // ala Schlick interpoliation
return lerp(F0, F90, t);
}

half3 DiffuseFromMetallic (half3 albedo, half metallic)
{
float oneMinusReflectivity = OneMinusReflectivityFromMetallic(metallic);
return albedo * oneMinusReflectivity;
}

half3 SpecularFromMetallic (half3 albedo, half metallic)
{
return lerp (unity_ColorSpaceDielectricSpec.rgb, albedo, metallic);
}

参考

  • Substance Docs,Shader API,2021
  • Razor Yang,简单聊聊CODM的图形,2021
  • harayoki,【SubstancePainter】編集中のシェーダーをすぐさまリロードする, 2019
  • nagnae,Substance Painter’s shader for Unity,2018
  • 月光下的旅行,Substance Painter Shader与UE4移动端渲染效果同步2020
  • IanBanks, arma 3 substance shader, 2018
分享