giving something back to the Flash community

AGAL distortions

Tags: , , , , ,

AGAL distortions

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 each present() 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.

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"	+

						"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.

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" +
						"dp4 	vt2		vt0		vc4		\n" +
						"cos	vt3		vt2				\n" +
						"sin	vt3		vt3				\n" +
						"add	vt1		vt0		vt3		\n" +
						"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”.


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";
		case Vector3D.Y_AXIS:
			shader		+=	"mul		vt4 		vc4 vt4.y			\n";
		case Vector3D.Z_AXIS:
			shader		+=	"mul		vt4 		vc4 vt4.z			\n";

	//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
	switch( this.axis )
		case Vector3D.X_AXIS:
			shader += 	"sub		vt1.y		vt2.y		vt3.z		\n" +
						"add		vt1.z		vt3.y		vt2.z		\n"	;
		case Vector3D.Y_AXIS:
			shader += 	"sub		vt1.x		vt2.x		vt3.z		\n" +
						"add		vt1.z		vt3.x		vt2.z		\n"	;
		case Vector3D.Z_AXIS:
			shader += 	"sub		vt1.x		vt2.x		vt3.y		\n" +
						"add		vt1.y		vt3.x		vt2.y		\n"	;

	// 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,

Tags: , , , , ,

© 2009 HIDIHO!. All Rights Reserved.

This blog is powered by Wordpress and Magatheme by Bryan Helmig.