shaders are the core of modern graphics ; they alter large streams of data very quickly.
the counterpart is that they are extremely stupid, especially in AGAL…
by stupid I mean limited and all their power lie in this limitedness or if you prefer, shaders don’t do much but they do it fast.
talking about limitations, here’s what we have:
Resource | Number allowed | Total memory |
---|---|---|
Vertex buffers |
4096 | 256 MB |
Index buffers |
4096 | 128 MB |
Programs |
4096 | 16 MB |
Textures |
4096 | 128 MB¹ |
Cube textures |
4096 | 256 MB |
so we can have up to 4096 instances of everything, seems a lot, we easily broke the vertexbuffer count on my last project ; recycling the resources is important. combining vertices, uvs, normals and other data within a single VertexBuffer3D seems to be the way to go. if need be I think the Texture limit can be bypassed by using the CubeTexture objects and forcing the normals to point along a fixed axis… just an idea.
AGAL limits: 200 opcodes per program.
so Adobe – surely for commendable reasons – chose to clamp the instructions’ count per shader to 200.
first I thought it would be a lot but it’s not. the Twist vertexShader already uses 18 opcodes : 10 % of the maximum possible shit : ouch. any light processing will take roughly 10 lines, most of it on the fragmentShader, which says a lot on how fast you’ll get screwed.
again during my last project, we broke that limit by adding a fifth light to our model… 5 lights…
Draw call limits: 32,768
drawTriangles()
calls for eachpresent()
call.
well, that sounds reasonnable… not sure though.
what about the opcodes restrictions?
Name | per fragment shader | per vertex shader | written |
---|---|---|---|
Attribute | n/a | 8 | VA# |
Constant | 28 | 128 | FC# / VC# |
Temporary | 8 | 8 | FT# / VT# |
Output | 1 | 1 | OC / OP |
Varying | 8 | 8 | V# |
Sampler | 8 | n/a | FS# |
so we can have 8 variables… funny joke. reusing them is really a pain in the butt. so far, 128 constants was enough, I haven’t really worked with the fragments yet but I have a feeling that 28 slots will be a bit tight. attributes registers (VA#, vertexBuffer3D) limitations haven’t been a problem so far but I’ve just done very basic things. the idea is to pack up as much as possible in there ( coords, uvs, normals etc.) which in turn might break the maximum size limitations described above… endless joy.
something not mentionned here : there’s no way to feed a varying registers ( V# ) from the fragmentshader (it’s possible the other way around). the vertexbuffers are never altered by the shaders so we can’t store the shader’s results ; there are no persistent values from one render pass to the next. it’s not possible to keep track of the result of a modified constant ( VC# / VT# ) apart from using a texture to store the data. haven’t tried it yet but it seems very useful and very tricky to implement.
there are no functions, 4 conditionnal operators and of course, no loops because real men don’t loop.
more generally, GPU programing is a martial art and there’s a fruitful activity around the “how not to get screwed” topic. the good news is that you’ll find a lot of resources, the bad news is that most of the time, the solutions found in GLSL or DirectX will not fit within AGAL’s restrictions. this made me admire all the efforts put into the 3D engines.
VERTEX SHADER
one of the things I like the most in 3D is the distortions. setting up some parametric modifiers, fiddling around and getting unexpected shapes is magic. it has no real use in real life but … you know… back in the days, Bartek did a nice API to distort meshes: As3Mod. it had a modifier stack and a VertexProxy that made it cross-engine.
so as an exercise, I’ve done a couple of modifiers.
I had to change slightly my previous “API” in order to dispatch something when a Model is done preparing and clearing the context. also changed AgalBase to Scene. the modifiers take a Model object as an input and know when to set up their own parameters. if need be, they can also keep a reference to their model then alter and recompile the vertexshader with some user input values. the Twist modifier for instance will depend greatly on the chosen AXIS, hence the re-compilation.
cut the crap Nicolas.
SPHERIFY MODIFIER (featuring Helmut the helmet):
click to rotate, “auto” if you’re too lazy to slide the phase, radius changes the sphere radius.
and the code goes like:
public function Spherify( model:Model ) { var shader:String = // use the raw coordinates (not projected) "mov vt0, va0 \n" + // normalize position "nrm vt0.xyz, vt0.xyz \n" + // "spherical" output (multiply norm by radius) "mul vt1 vt0 vc5 \n" + //lerp "sub vt0 vt1 va0 \n" + "mul vt0 vt0 vc4.x \n" + "add vt0 vt0 va0 \n" + // project "m44 op, vt0, vc0 \n" + // color output // normalize to smooth "nrm vt0.xyz, vt0.xyz \n" + "mov v0, vt0"; model.vertexShader = shader; model.addEventListener( Model.CONTEXT_PREPARED, this.prepareContext ); } public function prepareContext( e:Event ):void { Scene.setVertexVectorConstant( 4, Vector.<Number>([ _phase, 0,0, 1 ]) );//VC4 Scene.setVertexVectorConstant( 5, Vector.<Number>([ _radius, _radius, _radius, 1 ]) );//VC5 }
you’ve noticed the lerp 3liners, it stands for “linear interpolation” and it’s one of the most useful method in the universe. there are 2 other vital methods when it comes to CG, norm and map. that’s how you’d translate them into AGAL:
private function normalize( t:Number, min:Number, max:Number):Number { return ( t - min) / (max - min); } // = "sub TMP T MIN \n" + "sub TMP0 MAX MIN \n" + "div TMP TMP TMP0 \n" + private function lerp( t:Number, min:Number, max:Number):Number { return min + ( max - min) * t; } // = "sub TMP MAX MIN \n" + "mul TMP TMP T \n" + "add TMP MIN TMP \n" + private function map(t:Number, min1:Number, max1:Number, min2:Number, max2:Number):Number { return lerp( normalize(t, min1, max1), min2, max2); } //uses the result of norm into lerp
I haven’t used the norm and map methods yet but soon, very soon. there is a NRM operator that turns vectors into unit vectors. map is useful to swap from one coordinates system to another.
WAVE MODIFIER
this one is a touch more complex than spherify but so much more interesting.
and here goes the code:
private var seed:Vector.<Number> = Vector.<Number>([ Math.random(), Math.random(), Math.random(), 1 ]); public function Wave( model:Model ) { model.vertexShader ="mov vt0 va0 \n" + //wave "dp4 vt2 vt0 vc4 \n" + "cos vt3 vt2 \n" + "sin vt3 vt3 \n" + "add vt1 vt0 vt3 \n" + //lerp "sub vt0 vt1 va0 \n" + "mul vt0 vt0 vc5 \n" + "add vt0 vt0 va0 \n" + "m44 vt0 vt0 vc0 \n" + "mov op vt0 \n" + "nrm vt0.xyz va0.xyz \n" + "mov v0 vt0"; } public function prepareContext( e:Event ):void { Scene.setVertexVectorConstant( 4, seed ); Scene.setVertexVectorConstant( 5, Vector.<Number>([ strength, strength, strength, strength ]) ); }
note that the seed is modulated by a multiplier, (imho) better looking results are achieved with lower frequencies (a low multiplier). here’s how I set the seed passed as VC4:
public function reseed():void { x = Math.random(); y = Math.random(); z = Math.random(); } public function set x( value:Number ):void { seed[ 0 ] = value * multiplier; } public function set y( value:Number ):void { seed[ 1 ] = value * multiplier; } public function set z( value:Number ):void { seed[ 2 ] = value * multiplier; } public function get x():Number{ return seed[ 0 ]; } public function get y():Number{ return seed[ 1 ]; } public function get z():Number{ return seed[ 2 ]; }
as such, the waves are computed from the center, a possible improvement would be to offset the center and / or to create a wave “box”.
TWIST MODIFIER
the code goes like :
private function computeShader():void { // use the raw coordinates (not projected) var shader:String ="mov vt0 va0 \n" + //creates an output position "mov vt1 vt0 \n" + //computes the angle "mov vt4 vt0 \n" + "nrm vt4.xyz vt4.xyz \n"; switch( this.axis ) { case Vector3D.X_AXIS: shader += "mul vt4 vc4 vt4.x \n"; break; case Vector3D.Y_AXIS: shader += "mul vt4 vc4 vt4.y \n"; break; case Vector3D.Z_AXIS: shader += "mul vt4 vc4 vt4.z \n"; break; } //cosine & sine of the angle shader += "mov vt2 vt4 \n" +//cos angle "cos vt2 vt2 \n" + "mul vt2 vt1 vt2 \n" +// vt2 => pos.x*ct, pos.y*ct, pos.z*ct "mov vt3 vt4 \n" +//sin angle "sin vt3 vt3 \n" + "mul vt3 vt1 vt3 \n" +// vt3 => pos.x*st, pos.y*st, pos.z*st ""; //magic switch( this.axis ) { case Vector3D.X_AXIS: shader += "sub vt1.y vt2.y vt3.z \n" + "add vt1.z vt3.y vt2.z \n" ; break; case Vector3D.Y_AXIS: shader += "sub vt1.x vt2.x vt3.z \n" + "add vt1.z vt3.x vt2.z \n" ; break; case Vector3D.Z_AXIS: shader += "sub vt1.x vt2.x vt3.y \n" + "add vt1.y vt3.x vt2.y \n" ; break; } // project shader += "m44 op vt1 vc0 \n" + "nrm vt0.xyz vt0.xyz \n" + "mov v0 vt0"; model.vertexShader = shader; } public function prepareContext( e:Event ):void { Scene.setVertexVectorConstant( 4, Vector.<Number>([ _phase * _angle, _phase * _angle, _phase * _angle, 1 ]) );//VC4 }
notice the switch statements to apply a distortion along a given axis. I’m pretty sure this could be handled with a single Matrix. while browsing some literature, I found that many transforms could be done with a Matrix. to close this article I’d like to share a small tip: if you pass a Matrix3D as VC0, you can transform a position either by doing
"m44 vt0 va0 vc0 \n" +
or by decomposing the operation as follow
//matrix multiplication "dp4 vt0.x va0 vc0 \n" + "dp4 vt0.y va0 vc1 \n" + "dp4 vt0.z va0 vc2 \n" + "dp4 vt0.w va0 vc3 \n" + //end of matrix multiplication
in other words, a Matrix is stored as 4 * 4-digits Vectors and the M44 instructions is a short hand for 4 inlined DP4 between 2 4-digits vectors. knowing the tight opcodes’ limit, it might sound a corny thing to do but it gives us some healthy control over the Matrix transforms inside the vertexshader.
enough for now,
enjoy
my beloved readers wrote…