如果是做次世代项目,我们会常常听到美术同事对引擎的吐槽:“为啥我在Substance里的效果是对的,一把资源导入引擎,我做的模型就丑陋不堪”。这就导致了一个问题——美术会在Substance和引擎之间来回“折腾”。这其实是一种没必要的损耗。我们内部的美术同事都如此懊恼,外包就更不用说了。他们根本就没有我们的引擎,他们做的效果一切都是以Substance内看到的为准。如果外包做的资源,我们这边收包回来导入引擎有问题。解决办法要么是自己改,要么是叫外包改。但是如果外放期限将近,我们没时间改怎么办呢?于是就衍生出了一种解决方法,那就是疯狂的规定死美术资源生产流程,列好几十页密密麻麻的制作规范。那么问题来了,外包或者我们内部的美术制作真的能按照这么一大片规范严格执行每一条来制作美术资源吗,答案是否定的,很多项目的反馈已经证明了这一点。究其根本原因就是引擎和制作工具的效果不统一。
下面就以Substance Painter和max为例来演示一下同步。在Substance Painter中没有各向异性材质。比如我们要做一个头发,美术几乎可以说是在盲做。下面演示一下流程:
首先我们要将我们的模型,贴图和shader导入。
然后将Substance的的MainShader替换成我们导入的shader
最后我们就能在Substance里看到各向异性的材质啦。其实关键是这个shader是怎么写的。
//- Channels needed for metal/rough workflow are bound here.
//: param auto channel_basecolor
uniform
sampler2D
basecolor_tex;
//: param auto channel_roughness
uniform
sampler2D
roughness_tex;
//: param auto channel_metallic
uniform
sampler2D
metallic_tex;
//: param auto channel_specularlevel
uniform
sampler2D
specularlevel_tex;
//: param auto environment_rotation
uniform
float
cubemap_rotation;
//- Shader entry point.
vec4
shade
(V2F inputs)
{
vec4
OUT
=
vec4
(
0.0
f,
0.0
f,
0.0
f,
1.0
f);
vec3
light_pos
=
vec3
(
10.0
*
cos
(cubemap_rotation
*
2.0
*
M_PI),
10.0
,
10.0
*
sin
(cubemap_rotation
*
2.0
*
M_PI));
// Apply parallax occlusion mapping if possible
vec3
viewTS
=
worldSpaceToTangentSpace
(
getEyeVec
(inputs.
position
), inputs);
inputs.
tex_coord
+=
getParallaxOffset
(inputs.
tex_coord
, viewTS);
vec2
UV
=
inputs.
tex_coord
;
vec3
N
=
normalize
(inputs.
normal
);
vec3
T
=
inputs.
tangent
;
vec3
B
=
inputs.
bitangent
;
vec3
V
=
getEyeVec
(inputs.
position
);
vec3
L
=
normalize
(light_pos
-
inputs.
position
);
vec3
H
=
normalize
(V
+
L);
float
NoV
=
clamp
(
0
,
1
,
dot
(N,V));
float
NoL
=
dot
(N,L);
float
NoH
=
dot
(N,H);
// Fetch material parameters, and conversion to the specular/glossiness model
float
glossiness
=
1.0
-
getRoughness
(roughness_tex, inputs.
tex_coord
);
vec3
baseColor
=
getBaseColor
(basecolor_tex, inputs.
tex_coord
);
float
metallic
=
getMetallic
(metallic_tex, inputs.
tex_coord
);
float
specularLevel
=
getSpecularLevel
(specularlevel_tex, inputs.
tex_coord
);
vec3
diffColor
=
generateDiffuseColor
(baseColor, metallic);
vec3
specColor
=
generateSpecularColor
(specularLevel, baseColor, metallic);
// Get detail (ambient occlusion) and global (shadow) occlusion factors
float
AO
=
getAO
(inputs.
tex_coord
)
*
getShadowFactor
();
vec3
TTex
=
baseColor;
vec3
Tweight
=
(TTex
-
vec3
(
0.5
,
0.5
,
0.5
))
*
2
;
vec3
BaseT
=
normalize
(Tweight.
x
*
T
+
Tweight.
y
*
B);
T
=
normalize
(BaseT
+
N
*
(Tweight.
z
*
5
));
float
ToH
=
dot
(T,H);
float
ToH2
=
ToH
*
ToH;
float
TsH
=
sqrt
(
1
-
ToH2);
float
parm
=
pow
(TsH ,
100
);
vec3
spec
=
parm.
xxx
;
OUT.
rgb
=
NoL.
xxx
+
spec;
// Feed parameters for a physically based BRDF integration
return
OUT;
}
//- Entry point of the shadow pass.
void
shadeShadow
(V2F inputs)
{
}
下面就来一行一行介绍。
首先是采样器的声明,这个没什么好说的,写过GLSL的朋友都应该直到该怎么写了。这里需要注意的这些声明的注释
//- Channels needed for metal/rough workflow are bound here.
//: param auto channel_basecolor
uniform
sampler2D
basecolor_tex;
//: param auto channel_roughness
uniform
sampler2D
roughness_tex;
//: param auto channel_metallic
uniform
sampler2D
metallic_tex;
//: param auto channel_specularlevel
uniform
sampler2D
specularlevel_tex;
//: param auto environment_rotation
uniform
float
cubemap_rotation;
这里面//:符号可不是注释哦,这里的//:部分会加入到shader的编译。这是比较奇葩的一点。剩下就是像素着色器部分了。像素着色器里面也是非常中规中矩的写法,非常简单,但是需要注意的是灯光计算这一项。
vec3
light_pos
=
vec3
(
10.0
*
cos
(cubemap_rotation
*
2.0
*
M_PI),
10.0
,
10.0
*
sin
(cubemap_rotation
*
2.0
*
M_PI));
vec3
L
=
normalize
(light_pos
-
inputs.
position
);
这里的计算有点奇葩。因为SubstancePainter里面其实是没有灯光方向概念的,SubstancePainter的灯光默认是平着打过来的。所以L的向量方向(Pich和Roll)是规定死的,只有Yaw可以转。所以灯光方向其实只是一个Yaw的角度值。我们需要根据这个角度值来拿到灯光的方向向量L。
至此你已经知道怎么在Substance里面写shader了。一后就是自由发挥啦。我这里只是抛砖引玉。
最后补一句,shader是GLSL文件,直接能导入Substance,像导入普通模型资源的方式一样导入进去即可。