目前绝大多数的webgl库都使用字符串替换,并使用预编译指令来控制shader行为。
这种一般的基于字符串替换的方案存在以下缺陷:
- 没有可组合性,只是复用代码,抽象能力弱
- 代码包含大量不需要的实现,使用 #include #define 等做区分,调试和阅读困难, 开发效率低
- 问题定位只依赖最后编译报错,排错效率低
- 修改代码需要在浏览器运行所有依赖的shader,代码难维护
这些缺陷导致的各自难受之处之前也深有体会, 所以在设计artgl的shader体系时,我一直想要解决这个问题:
如何找到更好的方案来管理一个webgl框架内的shader代码?
glslify 可能是一个好的解决方案,但我个人并不是很喜欢这种方式: 在shader里写require, transform shader code。 这种方式是以shader code作为核心,而我希望以框架code为核心,这样做1是ts代码能做更多的事情,而shader只是字符串,即便使用了完整的shader parser,能做的事情还是比较有限,扩展起来也不方便。 最好是使用ts代码,框架的代码,对着色流程进行抽象,shader只是这种抽象的target。2是,glslify太重了,我还是倾向于暂时把事情看的简单一些。
正常我们写shader,大多数写的是一些function,然后组合起来,执行出结果。所以我设计的组合单元是、ShaderFunction: 比如下面这个shader function,pack float depth到 四通道 rgba中
1 2 3 4 5 6 7 8 9 10 11 12
| export const depthPackFunction = new ShaderFunction({ description: 'pack depth to RGBA output', source: ` vec4 depthPack(float frag_depth){ vec4 bitSh = vec4(256.0 * 256.0 * 256.0, 256.0 * 256.0, 256.0, 1.0); vec4 bitMsk = vec4(0.0, 1.0 / 256.0, 1.0 / 256.0, 1.0 / 256.0); vec4 enc = fract(frag_depth * bitSh); enc -= enc.xxyz * bitMsk; return enc; } `, })
|
ShaderFunction 只是描述这个shader的function,一个shader可能会调用多次shaderFunction进行计算,所以某个使用ShaderFunction的计算我们用 ShaderFunctionNode 来表示:ShaderFunction 事实上是 ShaderFunctionNode 的 factory。!
1
| const packedDepth = depthPackFunction.make()
|
function的调用总归要传参吧:参数从哪里来呢? 要么是来自shader外部,比如uniform,attribute,texture,对于片元着色器还有vary,要么来自其他function调用的结果。
我们先看外部的参数可以如何如何构造:
1 2 3 4 5 6 7 8 9 10 11 12
|
const VPMatrix = innerUniform(InnerSupportUniform.VPMatrix)
const sampleCount = uniform("u_sampleCount", GLDataType.float).default(0);
const position = attribute("position", GLDataType.floatVec3);
const depth = vary("depth", GLDataType.float);
|
然后我们可以给 packedDepth 喂数据了
1 2 3
| packedDepth = depthPackFunction.make().input( vary("depth", GLDataType.float) )
|
当然,链式调用,我们可以一个一个input, 栗子:
1 2 3 4
| const worldPosition = MVPTransform.make() .input("VPMatrix", innerUniform(InnerSupportUniform.VPMatrix)) .input("MMatrix", innerUniform(InnerSupportUniform.MMatrix)) .input("position", attribute("position", GLDataType.floatVec3))
|
如果 ShaderFunctionNode 依赖其他 ShaderFunctionNode 的计算结果,这也是可以的:
1 2 3 4 5 6 7 8 9 10 11 12 13
| const Random2D1 = rand2DT.make() .input("cood", vUV) .input("t", sampleCount)
const Random2D2 = rand.make() .input("n", Random2D1)
const randDir = dir3D.make() .input("x", Random2D1) .input("y", Random2D2)
|
通过这些node的连接,我们构造了一个graph,现在有了这个graph,我们要把shader搞出来。
OK, 我们再回过头来看shader,我们需要的是shader,webgl shader,两个着色器:
对于顶点着色器,总归有个 gl_Position ,或者 vary 这些。很简单,我们只要指定说:哪个node作为gl_Position,或者哪个node最为 叫什么名字的 vary就可以了。
对于顶点着色器,总归有个 gl_FragColor ,或者 mrt的输出 这些。很简单,我们只要指定说:哪个node作为 gl_FragColor ,mrt就先从简不考虑了,虽然挺对称的。
因为其实我们 vary 是构建顶点着色器指定的,所以前面vary也应该从整个graph get,api要改一下。
这是个画normal颜色的shader,应该好理解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const graph = new ShaderGraph();
graph.reset() .setVertexRoot( MVPTransform.make() .input("VPMatrix", innerUniform(InnerSupportUniform.VPMatrix)) .input("MMatrix", innerUniform(InnerSupportUniform.MMatrix)) .input("position", attribute( { name: 'position', type: GLDataType.floatVec3, usage: AttributeUsage.position } )) ) .setVary("color", attribute( { name: 'normal', type: GLDataType.floatVec3, usage: AttributeUsage.normal } )) .setFragmentRoot( normalShading.make().input("color", this.graph.getVary("color")) )
|
为了开发方便,很多小功能也被加入进来,比如你可以这么写:而不用自己包装function
1 2
| const node1 = vec4(attribute("position", GLDataType.floatVec3), constValue(1)) const node2 = node1.swizzling("zyx")
|
这就是 artgl ShaderGraph API的核心接口啦,目前这么写,已经可以work了!
在通过这些接口构建graph的过程中,每一步都有运行时参数类型的检查。link错的地方很早就能发现,解决了: 「问题定位只依赖最后编译报错,排错效率低」 的问题
虽然不是静态的检查,至少可以保证只要function写的不错,能link就能运行。所以你可以直接把每个shader构建加在单测里,单测没挂shader就没挂,不用起浏览器环境。解决了: 「修改代码需要在浏览器运行所有依赖的shader,代码难维护」 的问题
在编译shader的过程中,linker只会收集使用到的function和input依赖,没有任何预处理指令干扰视线,所以不存在: 「代码包含大量不需要的实现,使用 #include #define 等做区分,调试和阅读困难, 开发效率低」 的问题
shader graph 每一个node其实就是一个有类型的数值节点,所以可组合性非常强。不同的node类型实现不同的功能,易于扩展,比字符串拼接水平高多了。graph更是提供了整个shader的抽象,解决了「没有可组合性,只是复用代码,抽象能力弱」的问题
框架内置提供了常用的function供用户使用,也可以从独立的package import,shader 代码也正式成为框架的可扩展实现之一。 基于这样的抽象,甚至能解决不同shader version 编译的问题,这个就不展开了//
基于动态link,使用这种api,构造一个shader,类似于使用react,构造一个view,其实是相似的,有很多想象空间。基本上是我认为是实现框架抽象shader代码的一个好的方向。当然目前的解法还有一些限制,一些问题还有待解决,一些特性还有待设计和支持,如果有反馈和建议欢迎在 https://github.com/mikialex/artgl 反馈和交流。
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
| export class TSSAOShading extends Shading { update() { const VPMatrix = innerUniform(InnerSupportUniform.VPMatrix); const sampleCount = uniform("u_sampleCount", GLDataType.float).default(0); const depthTex = texture("depthResult"); this.graph.reset() .setVertexRoot( vec4(attribute( { name: 'position', type: GLDataType.floatVec3, usage: AttributeUsage.position } ), constValue(1))) .setVary("v_uv", attribute( { name: 'uv', type: GLDataType.floatVec2, usage: AttributeUsage.uv } ))
const vUV = this.graph.getVary("v_uv"); const depth = unPackDepth.make().input("enc", depthTex.fetch(vUV))
const worldPosition = getWorldPosition.make() .input("uv", vUV) .input("depth", depth) .input("VPMatrix", VPMatrix) .input("VPMatrixInverse", uniform("VPMatrixInverse", GLDataType.Mat4).default(new Matrix4()))
const Random2D1 = rand2DT.make() .input("cood", vUV) .input("t", sampleCount) const Random2D2 = rand.make() .input("n", Random2D1) const randDir = dir3D.make() .input("x", Random2D1) .input("y", Random2D2)
const newPositionRand = newSamplePosition.make() .input("positionOld", worldPosition.swizzling("xyz")) .input("distance", uniform("u_aoRadius", GLDataType.float).default(1)) .input("dir", randDir)
const newDepth = unPackDepth.make() .input("enc", depthTex.fetch( NDCxyToUV.make() .input( "ndc", NDCFromWorldPositionAndVPMatrix.make() .input( "position", newPositionRand ).input( "matrix", VPMatrix ) ) ) )
this.graph.setFragmentRoot( tssaoMix.make() .input("oldColor", texture("AOAcc").fetch(vUV).swizzling("xyz")) .input("newColor", sampleAO.make() .input("depth", depth) .input("newDepth", newDepth) ) .input("sampleCount", sampleCount) ) } }
|