-
Notifications
You must be signed in to change notification settings - Fork 0
Visual Shader
最近我为开源渲染器BGFX实现了一个Shader Graph/Material Editor,我称呼它为Visual Shader。BGFX是一款跨平台的渲染库,目前支持Metal、DX11、DX12、OpenGL等。
着色图是一个表示Shader/Material的图形网络拓扑。他提供了一个基于节点的图形界面,允许设计人员在不编写任何代码的情况下,添加和连接节点来创建着色器。下面是一些知名游戏引擎自带的Shader Graph
最近我在使用iPad时,在App Store上发现Shade这款APP时,它是一款移动版的Shader Material Editor,使用它来创建材质变得更加容易,并且能够使我在非办公环境下依旧较好的使用效果。 所以当我觉得需要为前期的学习完成一款产品时,我决定实现一个跨平台的Shader Graph。 以下是一个使用Visual Shader的案例,展示了最终实现的使用Visual Shader 编辑一个PBR的模型:
对我而言,实现Visual Shader并不算简单。你可以找到的例子,通常太过于庞大复杂,并不适合个人实现。且大部分是基于OpenGL或者GLFW来实现的。但是我最初的设定是想实现一款跨平台的产品,经过调研,BGFX基本满足我的需求,跨平台,接口抽象简单,所以底层的渲染器最终选择了它。这篇文章中,我将展示我是如何来实现Visual Shader的。
基础框架:Shader Graph通过图网来表示一个片段着色器(Fragment Shader)。在Visual Shader中,我通过生成GLSL的片段着色器来完成,这个过程成为Visual Shader的编译过程。
在图中的每一个节点都是一个代码片段。例如node_gamma节点接收两个参数(颜色、gama值)并输出gama转换之后的输出。在GLSL中,它就是简单的代码块:colorout=pow(colorin,gama);。然后在Visual Shader的编译过程中,我们只是将节点关系解析,然后形成完整的程序。
每个着色器图都有一个主节点,它是着色器图的最终输出。不同的光照模型认为是不同的主节点。在Visual Shader中,我主要实现了几种简单的主节点,Diffuse,Toon,PBR,OrenNayar等。
Visual Shader的编译过程,我们从主节点开始,然后递归遍历输入输出,将输入输出传递到对应的节点代码块中,如果节点的输入没有连接,则我们认为它应当作为着色器的Uniform输入,即最终的材质输入值。递归这个过程,直到主节点被完整解析。
作为一个案例,我们试着按照上面的过程解析下面这个Shader Graph。
编译过程是这样进行的:
1.尝试解析主节点Emission(Master节点):
a.解析输入参数Color。
i.解析MixRGB节点
x.解析输入参数Mix Type,因为它是枚举类型,在visual shader中一般会定义对应的函数入口,知道对应的函数node_mix_xx,这里使用node_mix_blend
y.解析输入参数Clamp,因为它没有连接点,所以将其申明为着色器的Uniform变量Clamp1
z.解析输入参数Fac,因为他没有连接点,所以将其申明为着色器的Uniform变量Fac1
w.解析输入参数Color1
Wi.解析RGB节点
Wix.解析输入参数Value,因为他没有连接点,所以将其申明为着色器的Uniform变量Value1
Wiy.声明输出参数Vec4 Color1
Wiz.调用节点代码块:node_rgb(value1,color1)
k.解析输入参数Color2
ki.解析RGB节点
kix.解析输入参数Value,因为他没有连接点,所以将其申明为着色器的Uniform变量Value2
kiy.声明输出参数Vec4 Color2
kiz.调用节点代码块:node_rgb(value2,color2)
p.声明输出参数Vec4 mix_color
q.调用节点代码块:node_mix_blend(Clamp1,Fac1,Color1,Color2,mix_color)
b.解析输入参数Strength,因为它没有连接点,所以将其申明为着色器的Uniform变量Strength1
c.声明输出参数Vec4 Emission
d.调用节点代码块:node_emission(mix_color,Strength1,Emission)
f.输出着色器结果gl_FragColor = Emission
最终生成的Fragment代码可能像下面这样:
#version 450
// other uniforms/attributes here...
uniform float Clamp1;
uniform float Fac1;
uniform vec3 Value1;
uniform vec3 Value2;
uniform float Strength1;
void node_rgb(vec3 val,inout vec4 col){
color = vec4(val,1.0);
}
void mix_blend(float use_clamp, float fac, vec4 col1, vec4 col2, out vec4 outcol)
{
fac = clamp(fac, 0.0, 1.0);
outcol = mix(col1, col2, fac);
outcol.a = col1.a;
}
void node_emission(vec4 val,float strength ,inout vec4 col){
col = val*strength;
}
void main()
{
// pre code...
vec4 Color1;
node_rgb(Value1,Color1);
vec4 Color2;
node_rgb(Value2,Color2);
Vec4 mix_color;
node_mix_blend(Clamp1,Fac1,Color1,Color2,mix_color);
vec4 Emission;
node_emission(mix_color,Strength1,Emission)
gl_FragColor = Emission;
// post code...
}
其中的节点代码块,可以写在独立的glsl文件中,这里为了描述清晰整合在一起方便浏览。有些节点可能有多个输出,这个只需要在对应的节点代码块提前定义出来即可。
function resolve_node(node,outIndex):
if node is resolved:
return node.result[outIndex]
let this_code = node.code
store_outputs(node) // create variables for each output
//repleace out var
for j in node.outs:
replace(this_code, j, store_outs[j])
//repleace out var
for i in node.inputs:
if i.connected_node
replace(this_code, i, resolve_node(i.connected_node,i.connected_slot_index)))
else:
replace(this_code, i, node.uniform_slot)
write_code(this_code);
//return call need slot out
return store_outs[outIndex];
Visual Shader中Graph Editor实现方式是一种面向数据的设计结果,Graph只是保存节点和连接数据,Editor负责创建和修改图形,Compiler接收数据并输出片段着色器代码。
正如前面所述,Graph只保留节点和链接数据,这样比较容易存储序列化数据。基本数据结构如下:
enum class SlotType
{
FLOAT = 1, VEC2, VEC3, VEC4, SAMPLER2D, INVALID = 0
};
struct Slot
{
SlotType type{};
string value; // if unconnected
};
struct Node
{
string name;
Guid guid;
vec2 position;
vector<Slot> input_slots;
vector<Slot> output_slots;
};
struct Connect
{
Guid node_out;
Guid node_in;
int slot_out_index{};
int slot_in_index{};
};
struct Parameter
{
string name;
SlotType type{};
string default_value;
};
struct Graph
{
Guid master_node;
hash_table<Guid, Node> nodes;
vector<Connect> links;
vector<SlotType> parameters;
};
节点预览是Shader Graph的一个很重要的功能,它提供在节点阶段的输出值的预览。由于时间所限,在目前阶段Visual Shader暂时不提供该功能,但是我对它的工作原理还是有写了解的。 首先需要的是为想要预览的节点独立生成一个不同的着色器,将当前节点的输出值转成Vec4,将其作为颜色值返回即可。对于浮点数,我们将其转换为灰度图颜色值即可。 其实你需要与渲染器集成,将每个节点结果渲染到单独的一张图片中。
实现Visual Shader的过程是一件蛮有趣的事,这篇文章中我省略了很多细节,因为自己的Mac版本太低,无法输出iOS版本的APP,后期应该不会在继续该项目的迭代工作了。如果你希望为自己的渲染器或引擎实现一个Shader Graph,希望这篇文章对你有所帮助,另外该项目的源码我已上传至GitHub,如果你想了解更多细节,可以直接浏览源码。