switching colour alternativa3d

Posted by davidejones

A popular shader that I’ve seen used in away3d and flare3d is this sort of colour switching effect with a bulge. Thanks to the excellent post at http://blog.volger.org/switchingcolor-shader/ on how to create such an effect I’ve created an alternativa3d material using the same principles.

Note: Thanks to makc from the alternativa3d forums the position is now fixed, and the code below has been updated

Here is the finished material

package alternativa.engine3d.materials {

	import alternativa.engine3d.alternativa3d;
	import alternativa.engine3d.core.Camera3D;
	import alternativa.engine3d.core.DrawUnit;
	import alternativa.engine3d.core.Light3D;
	import alternativa.engine3d.core.Object3D;
	import alternativa.engine3d.core.Renderer;
	import alternativa.engine3d.core.Transform3D;
	import alternativa.engine3d.core.VertexAttributes;
	import alternativa.engine3d.materials.compiler.Linker;
	import alternativa.engine3d.materials.compiler.Procedure;
	import alternativa.engine3d.materials.compiler.VariableType;
	import alternativa.engine3d.objects.Surface;
	import alternativa.engine3d.resources.Geometry;
	import flash.events.TimerEvent;
	import flash.geom.Vector3D;
	import flash.utils.Timer;
	import flash.utils.getTimer;

	import flash.display3D.Context3D;
	import flash.display3D.Context3DBlendFactor;
	import flash.display3D.Context3DProgramType;
	import flash.display3D.VertexBuffer3D;
	import flash.utils.Dictionary;

	use namespace alternativa3d;

	/**
	 * The material fills surface with solid color in light-independent manner. Can draw a Skin with no more than 41 Joints per surface. See Skin.divide() for more details.
	 *
	 * @see alternativa.engine3d.objects.Skin#divide()
	 */
	public class SwitchFillMaterial extends Material {
		
		private static var caches:Dictionary = new Dictionary(true);
		private var cachedContext3D:Context3D;
		private var programsCache:Dictionary;

		// Vertex Procedure
		private static const SwitchFillInsetProcedure:Procedure = getSwitchFillVProcedure(true);
		private static const SwitchFillOutsetProcedure:Procedure = getSwitchFillVProcedure(false);
		private static function getSwitchFillVProcedure(inset:Boolean):Procedure
		{
			var insetMode:String = (inset) ? "" : "neg t5, t5";
			return new Procedure([
				"#a0=aPosition",
				"#a1=aNormal",
				"#c4=cOrigin",
				"#c5=cFactors",
				"#c6=cMixed",
				"#c7=cMixed2",
				
				// calculate distance between current position and distance to color change origin
				// distance = length(iwposition.xyz - origin.xyz);				
				"sub t1 a0.xyz c4.xyz",
				"dp3 t1 t1 t1",
				"sqt t1 t1",
				
				// take position of current vertex and multiply it with the world
				// iwposition = float4 (position,1) * world;
				"mul t0 a0 c0",
				
				// adjust for aesthetics  
				// distance /= 3;
				"div t1 t1 c7.x",
				
				// move factors into v2
				"mov v2 c5",
				
				// calculate factors of current colors
				// f1 = saturate((distance - timedelta) / spread);
				// f2 = saturate(-((distance - timedelta) - spread) / spread);
				//f1
				"sub t2 t1 c6.x", // distance - timedelta
				"div t2 t2 c6.y", // / spread
				"sat t2 t2", 	  //saturate
				"mov v2.x t2",    // factors.x
				//f2
				"sub t3 t1 c6.x", // distance - timedelta
				"sub t3 t3 c6.y", // -spread
				"neg t3 t3",	  // multiply by -1 to get -((distance - timedelta) - spread)
				"div t3 t3 c6.y", // / spread
				"sat t3 t3",      // saturate
				"mov v2.y t3",    // factors.y 

				//mynormal = float4 (normal * world,1) * -sin(f1 * f2 / 2);
				"mul t4 a1 c0",   // normal * world				
				"mul t5 t2 t3",   // f1 * f2
				"div t5 t5 c6.w", // /2
				"sin t5 t5",	  // sin
				insetMode,	  // neg to get -sin
				"mul t6 t4 t5",  // multiply 
				
				//iwposition = float4 (position,1) + mynormal;
				"add t0 a0 t5",		
				
				// iwposition * worldViewProj;	
				"m44 o0 t0 c0",
			], "getSwitchFillVProcedure");
		}
		
		// Fragment procedure
		static alternativa3d const getSwitchFillFProcedure:Procedure = new Procedure([
			"#c0=cCurrentColor", 
			"#c1=cNewColor", 
			//color = factors.x * currentcolor;
			"mul t0 v2.x c0",
			//color += factors.y * newcolor;
			"mul t1 v2.y c1",
			"add t0, t0, t1",
			//output
			"mov o0, t0"
		], "getSwitchFillFProcedure");
		
		/**
		 * Transparency
		 */
		public var alpha:Number = 1;
		
		private var red:Number;
		private var green:Number;
		private var blue:Number;
		
		private var nred:Number;
		private var ngreen:Number;
		private var nblue:Number;
		private var nalpha:Number = 1;
		
		private var currentColor:uint;
		private var newColor:uint;
		private var origin:Vector3D = new Vector3D(0, 0, 0, 1);
		private var factors:Vector3D = new Vector3D(0, 0, 0, 1);
		private var timedelta:Number = 0.5;
		private var spread:Number = 6;
		private var startTime:Number = 0;
		private var timeStretch:Number = 40;
		private var inset:Boolean = false;
		
		/**
		 * Color.
		 */
		public function get color():uint {
			return (red*0xFF << 16) + (green*0xFF << 8) + blue*0xFF;
		}

		/**
		 * @private
		 */
		public function set color(value:uint):void {
			red = ((value >> 16) & 0xFF)/0xFF;
			green = ((value >> 8) & 0xFF)/0xFF;
			blue = (value & 0xff)/0xFF;
		}

		/**
		 * Creates a new SwitchFillMaterial instance.
		 * @param color Color .
		 * @param alpha Transparency.
		 */
		public function SwitchFillMaterial(color:uint = 0x7F7F7F, alpha:Number = 1, timeStretch:Number = 40, inset:Boolean = false) {
			this.color = color;
			this.alpha = alpha;
			this.nalpha = nalpha;
			this.inset = inset;
			
			this.timeStretch = timeStretch;
			this.currentColor = color;
			this.newColor = color;
			nred = ((this.newColor >> 16) & 0xFF)/0xFF;
			ngreen = ((this.newColor >> 8) & 0xFF)/0xFF;
			nblue = (this.newColor & 0xff) / 0xFF;
			
			startTime = getTimer();
		}
		
		public function switchColor(color:uint = 0x7F7F7F, neworigin:Vector3D=null):void
		{
			this.currentColor = this.newColor;
			red = ((this.currentColor >> 16) & 0xFF)/0xFF;
			green = ((this.currentColor >> 8) & 0xFF)/0xFF;
			blue = (this.currentColor & 0xff)/0xFF;
			
			this.newColor = color;
			nred = ((this.newColor >> 16) & 0xFF)/0xFF;
			ngreen = ((this.newColor >> 8) & 0xFF)/0xFF;
			nblue = (this.newColor & 0xff) / 0xFF;
			
			if (neworigin != null)
			{
				this.origin = neworigin;
			}
			startTime = getTimer() - 1;
		}
		
		public function render():void
		{
			timedelta = (getTimer() - this.startTime) / this.timeStretch;
		}
		
		private function setupProgram(object:Object3D):SwitchFillMaterialProgram {
			var vertexLinker:Linker = new Linker(Context3DProgramType.VERTEX);
			var positionVar:String = "aPosition";
			var normalVar:String = "aNormal";
			vertexLinker.declareVariable(positionVar, VariableType.ATTRIBUTE);
			vertexLinker.declareVariable(normalVar, VariableType.ATTRIBUTE);
			
			var proc:Procedure = (this.inset) ? SwitchFillInsetProcedure : SwitchFillOutsetProcedure;
			
			proc.assignVariableName(VariableType.CONSTANT, 0, "cProjMatrix", 4);
			if (object.transformProcedure != null) {
				positionVar = appendPositionTransformProcedure(object.transformProcedure, vertexLinker);
			}
			//vertexLinker.addProcedure(_projectProcedure);
			//vertexLinker.setInputParams(_projectProcedure, positionVar);
			vertexLinker.addProcedure(proc);
			vertexLinker.setInputParams(proc, positionVar);
			vertexLinker.setInputParams(proc, normalVar);

			var fragmentLinker:Linker = new Linker(Context3DProgramType.FRAGMENT);
			//fragmentLinker.addProcedure(outColorProcedure);
			fragmentLinker.addProcedure(getSwitchFillFProcedure);
			fragmentLinker.varyings = vertexLinker.varyings;
			return new SwitchFillMaterialProgram(vertexLinker, fragmentLinker);
		}

		/**
		 * @private 
		 */
		override alternativa3d function collectDraws(camera:Camera3D, surface:Surface, geometry:Geometry, lights:Vector., lightsLength:int, useShadow:Boolean, objectRenderPriority:int = -1):void {
			var object:Object3D = surface.object;
			// Strams
			var positionBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.POSITION);
			var normalsBuffer:VertexBuffer3D = geometry.getVertexBuffer(VertexAttributes.NORMAL);
			
			// Check validity
			if (positionBuffer == null) return;
			// Program

			// Renew program cache for this context
			if (camera.context3D != cachedContext3D) {
				cachedContext3D = camera.context3D;
				programsCache = caches[cachedContext3D];
				if (programsCache == null) {
					programsCache = new Dictionary();
					caches[cachedContext3D] = programsCache;
				}
			}

			var program:SwitchFillMaterialProgram = programsCache[object.transformProcedure];
			if (program == null) {
				program = setupProgram(object);
				program.upload(camera.context3D);
				programsCache[object.transformProcedure] = program;
			}
			// Drawcall
			var drawUnit:DrawUnit = camera.renderer.createDrawUnit(object, program.program, geometry._indexBuffer, surface.indexBegin, surface.numTriangles, program);
		
			// Streams
			// a0 a1
			drawUnit.setVertexBufferAt(program.aPosition, positionBuffer, geometry._attributesOffsets[VertexAttributes.POSITION], VertexAttributes.FORMATS[VertexAttributes.POSITION]);
			drawUnit.setVertexBufferAt(program.aNormal, normalsBuffer, geometry._attributesOffsets[VertexAttributes.NORMAL], VertexAttributes.FORMATS[VertexAttributes.NORMAL]);
				
			// Constants
			object.setTransformConstants(drawUnit, surface, program.vertexShader, camera);
			
			drawUnit.setProjectionConstants(camera, program.cProjMatrix, object.localToCameraTransform); //c0
			drawUnit.setVertexConstantsFromNumbers(program.cOrigin, origin.x, origin.y, origin.z, origin.w); //c4
			drawUnit.setVertexConstantsFromNumbers(program.cFactors, factors.x, factors.y, factors.z, factors.w); //c5
			drawUnit.setVertexConstantsFromNumbers(program.cMixed, timedelta, spread, -1, 2); //c6
			drawUnit.setVertexConstantsFromNumbers(program.cMixed2, 3, 1, 0, 0); //c7
			
			//fragment constants
			drawUnit.setFragmentConstantsFromNumbers(program.cCurrentColor, red, green, blue, alpha); //c0
			drawUnit.setFragmentConstantsFromNumbers(program.cNewColor, nred, ngreen, nblue, nalpha); //c1
			
			// Send to render
			if (alpha < 1) {
				drawUnit.blendSource = Context3DBlendFactor.SOURCE_ALPHA;
				drawUnit.blendDestination = Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA;
				camera.renderer.addDrawUnit(drawUnit, objectRenderPriority >= 0 ? objectRenderPriority : Renderer.TRANSPARENT_SORT);
			} else {
				camera.renderer.addDrawUnit(drawUnit, objectRenderPriority >= 0 ? objectRenderPriority : Renderer.OPAQUE);
			}
		}

		/**
		 * @inheritDoc 
		 */
		override public function clone():Material {
			var res:SwitchFillMaterial = new SwitchFillMaterial(color, alpha);
			res.clonePropertiesFrom(this);
			return res;
		}

	}
}

import alternativa.engine3d.materials.ShaderProgram;
import alternativa.engine3d.materials.compiler.Linker;

import flash.display3D.Context3D;

class SwitchFillMaterialProgram extends ShaderProgram {

	public var aPosition:int = -1;
	public var aNormal:int = -1;
	public var cProjMatrix:int = -1;
	public var cOrigin:int = -1;
	public var cCurrentColor:int = -1;
	public var cNewColor:int = -1;
	public var cFactors:int = -1;
	public var cMixed:int = -1;
	public var cMixed2:int = -1;

	public function SwitchFillMaterialProgram(vertex:Linker, fragment:Linker) {
		super(vertex, fragment);
	}

	override public function upload(context3D:Context3D):void {
		super.upload(context3D);

		aPosition =  vertexShader.findVariable("aPosition");
		aNormal = vertexShader.findVariable("aNormal");
		cProjMatrix = vertexShader.findVariable("cProjMatrix");
		cOrigin = vertexShader.findVariable("cOrigin");
		cFactors = vertexShader.findVariable("cFactors");
		cMixed = vertexShader.findVariable("cMixed");
		cMixed2 = vertexShader.findVariable("cMixed2");
		
		cCurrentColor = fragmentShader.findVariable("cCurrentColor");
		cNewColor = fragmentShader.findVariable("cNewColor");
	}
}

And here is its usage

package 
{
	import alternativa.engine3d.core.events.MouseEvent3D;
	import alternativa.engine3d.core.RayIntersectionData;
	import alternativa.engine3d.objects.Mesh;
	import flash.geom.Matrix3D;

	import alternativa.engine3d.primitives.Box;
	import alternativa.engine3d.controllers.SimpleObjectController;
	import alternativa.engine3d.core.Camera3D;
	import alternativa.engine3d.core.Object3D;
	import alternativa.engine3d.core.Resource;
	import alternativa.engine3d.core.View;
	
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.geom.Vector3D;
	import flash.display.Stage3D;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	
	import alternativa.engine3d.materials.FillMaterial;
	import alternativa.engine3d.materials.SwitchFillMaterial;
	
	import alternativa.engine3d.alternativa3d;
	use namespace alternativa3d;
	
	/**
	 * ...
	 * @author David E Jones
	 */
	 
	public class Main extends Sprite 
	{
		private var scene:Object3D = new Object3D();
		private var camera:Camera3D;
		private var controller:SimpleObjectController;
		private var stage3D:Stage3D;
		private var b:Box;
		private var smat:SwitchFillMaterial;
		
		public function Main():void 
		{
			if (stage) init();
			else addEventListener(Event.ADDED_TO_STAGE, init);
		}
		
		private function init(e:Event = null):void 
		{
			removeEventListener(Event.ADDED_TO_STAGE, init);
			
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
			
			camera = new Camera3D(1, 10000);
			camera.view = new View(stage.stageWidth, stage.stageHeight, false, 0x1d1d1d, 0, 4);
			addChild(camera.view);
			addChild(camera.diagram);

			camera.x = -45;
			camera.y = -40;
			camera.z = 60;
			controller = new SimpleObjectController(stage, camera, 200);
			controller.lookAt(new Vector3D(0,0,0));
			
			scene.addChild(camera);
			
			stage3D = stage.stage3Ds[0];
			stage3D.addEventListener(Event.CONTEXT3D_CREATE, onContextCreate);
			stage3D.requestContext3D();
		}
		
		private function onContextCreate(e:Event):void {
			stage3D.removeEventListener(Event.CONTEXT3D_CREATE, onContextCreate);
			
			setupSceneObjects();
			
			stage.addEventListener(Event.ENTER_FRAME, onEnterFrame);
			stage.addEventListener(Event.RESIZE, onResize);
			onResize();
		}
		
		private function setupSceneObjects():void
		{
			smat = new SwitchFillMaterial(0xFF0000, 1, 40, false);
			
			b = new Box(50, 50, 10, 10, 10, 10, false, null);
			b.setMaterialToAllSurfaces(smat);
			uploadResources(b.getResources(true));
			scene.addChild( b );
			
			b.addEventListener(MouseEvent3D.CLICK, onClick);
		}
		
		private function onClick(e:MouseEvent3D):void
		{		
			var local_point:Vector3D = new Vector3D(e.localX, e.localY, e.localZ);
			var col:uint = Math.random() * 0xFFFFFF;
			smat.switchColor(col, local_point);
		}

		private function uploadResources(resources:Vector.):void {
			for each (var resource:Resource in resources) {
				resource.upload(stage3D.context3D);
			}
		}

		private function onEnterFrame(e:Event):void 
		{
			controller.update();
			smat.render();
			camera.render(stage3D);
		}
		
		private function onResize(e:Event = null):void {
			camera.view.width = stage.stageWidth;
			camera.view.height = stage.stageHeight;
		}
		
	}
	
}

You can download the example files here

Trackback URL for this post: http://davidejones.com/blog/1599-switching-colour-alternativa3d/trackback/

Being Sociable...

  • If you like this article then please share it on your favourite social network and follow me on twitter for the latest updates

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>