Browse Source

Implement new s2d camera (#698)

Pavel Alexandrov 5 years ago
parent
commit
9fb2ff1daa
7 changed files with 883 additions and 372 deletions
  1. 394 93
      h2d/Camera.hx
  2. 0 1
      h2d/Layers.hx
  3. 35 28
      h2d/Object.hx
  4. 153 91
      h2d/RenderContext.hx
  5. 184 64
      h2d/Scene.hx
  6. 6 5
      h3d/shader/Base2d.hx
  7. 111 90
      samples/Camera2D.hx

+ 394 - 93
h2d/Camera.hx

@@ -1,121 +1,422 @@
 package h2d;
 package h2d;
 
 
-class Camera extends h2d.Object {
-
-	/** X position of the camera anchor point. **/
-	public var viewX(get, set) : Float;
-	/** Y position of the camera anchor point. **/
-	public var viewY(get, set) : Float;
-
-	/** Current camera width. Cannot be set directly, please use `horizontalRatio`. **/
-	public var width(default, null) : Int;
-	/** Current camera height. Cannot be set directly, please use `verticalRatio`. **/
-	public var height(default, null) : Int;
-	/** Percent value of horizontal camera size relative to s2d size. (default : 1) **/
-	public var horizontalRatio(default, set) : Float;
-	/** Percent value of vertical camera size relative to s2d size. (default : 1) **/
-	public var verticalRatio(default, set) : Float;
-	/** Horizontal anchor position inside ratio boundaries used for anchoring and resize compensation. ( default : 0.5 ) **/
+/**
+	2D camera instance. Allows for positioning, scaling and rotation of 2D objects on the scene.
+	Per-layer visibility can be configured by overriding `layerVisible` method.
+	Camera can clip out contents outside of it's viewport by setting `clipViewport` to `true`.
+	Due to Heaps event handling structure, only one camera can handle scene events, and can be set with `h2d.Scene.interactiveCamera`.
+	When handling events, assigned camera isn't checked for it's nor layers visibiilty.
+	Camera system is circumvented if Scene would get any filter assigned to it.
+**/
+@:access(h2d.RenderContext)
+@:access(h2d.Scene)
+@:allow(h2d.Scene)
+class Camera {
+
+	/**
+		X position of the camera in world space based on anchorX.
+	**/
+	public var x(default, set) : Float;
+	/**
+		Y position of the camera in world space based on anchorY.
+	**/
+	public var y(default, set) : Float;
+
+	/**
+		Horizontal scale factor of the camera. Scaling applied, using anchored position as pivot.
+	**/
+	public var scaleX(default, set) : Float;
+	/**
+		Vertical scale factor of the camera. Scaling applied, using anchored position as pivot.
+	**/
+	public var scaleY(default, set) : Float;
+
+	/**
+		Rotation of the camera in radians. Camera is rotated around anchored position.
+	**/
+	public var rotation(default, set) : Float;
+
+	/**
+		Enables viewport clipping. Allow to restrict rendering area of the camera.
+	**/
+	public var clipViewport : Bool;
+	/**
+		Horizontal viewport offset of the camera relative to internal scene viewport (see h2d.Scene.scaleMode) in scene coordinates. ( default : 0 )  
+		Automatically scales on scene resize.
+	**/
+	public var viewportX(get, set) : Float;
+	/**
+		Vertical viewport offset of the camera relative to internal scene viewport (see h2d.Scene.scaleMode) in scene coordinates. ( default : 0 )  
+		Automatically scales on scene resize.
+	**/
+	public var viewportY(get, set) : Float;
+	/**
+		Camera viewport width in scene coordinates. ( default : scene.width )  
+		Automatically scales on scene resize.
+	**/
+	public var viewportWidth(get, set) : Float;
+	/**
+		Camera viewport height in scene coordinates. ( default: scene.height )  
+		Automatically scales on scene resize.
+	**/
+	public var viewportHeight(get, set) : Float;
+
+	/**
+		Horizontal anchor position inside viewport boundaries used for positioning and resize compensation. ( default : 0 )  
+		Value is a percentile (0..1) from left viewport edge to right viewport edge with 0.5 being center.
+	**/
 	public var anchorX(default, set) : Float;
 	public var anchorX(default, set) : Float;
-	/** Vertical anchor position inside ratio boundaries used for anchoring and resize compensation. ( default : 0.5 ) **/
+	/**
+		Vertical anchor position inside viewport boundaries used for positioning and resize compensation. ( default : 0 )  
+		Value is apercentile (0..1) from top viewport edge to bottom viewport edge with 0.5 being center.
+	**/
 	public var anchorY(default, set) : Float;
 	public var anchorY(default, set) : Float;
 
 
-	var ratioChanged : Bool;
-	var sceneWidth : Int;
-	var sceneHeight : Int;
-	var anchorWidth : Float;
-	var anchorHeight : Float;
+	/** Camera visibility. Does not affect event handling for interactive camera. **/
+	public var visible : Bool;
+
+	/** Set to enable primitive position sync between camera and target Object. **/
+	public var follow : h2d.Object;
+	/** Syncs camera rotation to follow object rotation. **/
+	public var followRotation : Bool;
+
+	var posChanged : Bool;
+
+	var viewX : Float;
+	var viewY : Float;
+	var viewW : Float;
+	var viewH : Float;
+
+	var matA : Float;
+	var matB : Float;
+	var matC : Float;
+	var matD : Float;
+	var absX : Float;
+	var absY : Float;
+	var invDet : Float;
+
 	var scene : Scene;
 	var scene : Scene;
 
 
-	public function new( ?parent : h2d.Object, horizontalRatio : Float = 1, verticalRatio : Float = 1, anchorX : Float = 0.5, anchorY : Float = 0.5 ) {
-		super(parent);
-		this.horizontalRatio = horizontalRatio;
-		this.verticalRatio = verticalRatio;
-		this.anchorX = anchorX;
-		this.anchorY = anchorY;
-		this.width = 0; this.height = 0;
-		this.anchorWidth = 0; this.anchorHeight = 0;
-		this.sceneWidth = 0; this.sceneHeight = 0;
-		ratioChanged = true;
-		if ( parent != null ) {
-			initScene();
+	public function new( ?scene : Scene ) {
+		this.x = 0; this.y = 0;
+		this.scaleX = 1; this.scaleY = 1;
+		this.rotation = 0;
+		this.anchorX = 0;
+		this.anchorY = 0;
+		this.viewX = 0; this.viewY = 0;
+		this.viewW = 1; this.viewH = 1;
+		this.visible = true;
+		if (scene != null) scene.addCamera(this);
+	}
+
+	public inline function remove() {
+		if (scene != null) scene.removeCamera(this);
+	}
+
+	/**
+		Override this method to set visibility only to specific layers. Renders all layers by default.
+		Layer visibility is not checked during Interactive event handling.
+	**/
+	public dynamic function layerVisible( layer : Int ) : Bool {
+		return true;
+	}
+
+	@:noCompletion public function enter( ctx : RenderContext ) {
+		ctx.pushCamera(this);
+		if ( clipViewport ) {
+			var old = ctx.inFilter;
+			ctx.inFilter = null;
+			ctx.pushRenderZone(viewX * scene.width, viewY * scene.height, viewW * scene.width, viewH * scene.height);
+			ctx.inFilter = old;
 		}
 		}
 	}
 	}
 
 
-	inline function get_viewX() { return -this.x + anchorWidth; }
-	inline function get_viewY() { return -this.y + anchorHeight; }
+	@:noCompletion public function exit( ctx : RenderContext ) {
+		if ( clipViewport ) {
+			var old = ctx.inFilter;
+			ctx.inFilter = null;
+			ctx.popRenderZone();
+			ctx.inFilter = old;
+		}
+		ctx.popCamera();
+	}
+
+	@:access(h2d.Object)
+	public function sync( ctx : RenderContext, force : Bool = false )
+	{
+		if (scene == null) return;
 
 
-	inline function set_viewX( v : Float ) {
-		this.x = -v + anchorWidth;
-		return -v;
+		if ( follow != null ) {
+			this.x = follow.absX;
+			this.y = follow.absY;
+			if ( followRotation ) this.rotation = -follow.rotation;
+		}
+		if ( posChanged || force ) {
+			if ( rotation == 0 ) {
+				matA = scaleX;
+				matB = 0;
+				matC = 0;
+				matD = scaleY;
+			} else {
+				var cr = Math.cos(rotation);
+				var sr = Math.sin(rotation);
+				matA = scaleX * cr;
+				matB = scaleX * sr;
+				matC = scaleY * -sr;
+				matD = scaleY * cr;
+			}
+			absX = Math.round(-(x * matA + y * matC) + (scene.width * anchorX * viewW) + scene.width * viewX);
+			absY = Math.round(-(x * matB + y * matD) + (scene.height * anchorY * viewH) + scene.height * viewY);
+			invDet = 1 / (matA * matD - matB * matC);
+			posChanged = false;
+		}
 	}
 	}
-	inline function set_viewY( v : Float ) {
-		this.y = -v + anchorHeight;
-		return -v;
+
+	public inline function setScale( x : Float, y : Float ) {
+		this.scaleX = x;
+		this.scaleY = y;
 	}
 	}
 
 
-	inline function set_horizontalRatio( v ) {
-		ratioChanged = true;
-		return horizontalRatio = hxd.Math.clamp(v, 0, 1);
+	public inline function scale( x : Float, y : Float ) {
+		this.scaleX *= x;
+		this.scaleY *= y;
 	}
 	}
-	
-	inline function set_verticalRatio( v ) {
-		ratioChanged = true;
-		return verticalRatio = hxd.Math.clamp(v, 0, 1);
+
+	public inline function setPosition( x : Float, y : Float ) {
+		this.x = x;
+		this.y = y;
 	}
 	}
 
 
-	inline function set_anchorX( v ) {
-		anchorX = hxd.Math.clamp(v, 0, 1);
-		anchorWidth = sceneWidth * anchorX;
-		return anchorX;
+	public inline function move( dx : Float, dy : Float ) {
+		this.x += dx;
+		this.y += dy;
 	}
 	}
 
 
-	inline function set_anchorY( v ) {
-		anchorY = hxd.Math.clamp(v, 0, 1);
-		anchorHeight = sceneHeight * anchorY;
-		return anchorY;
+	public inline function rotate( v : Float ) {
+		this.rotation += v;
 	}
 	}
 
 
-	override private function onAdd()
-	{
-		initScene();
-		super.onAdd();
+	public inline function setAnchor( x : Float, y : Float ) {
+		this.anchorX = x;
+		this.anchorY = y;
 	}
 	}
 
 
-	override private function onRemove()
-	{
-		this.scene = null;
-		super.onRemove();
+	/**
+		Sets camera viewport dimensions. If `w` or `h` arguments are 0 - scene size is used (width or height respectively).
+	**/
+	public inline function setViewport( x : Float = 0, y : Float = 0, w : Float = 0, h : Float = 0 ) {
+		checkScene();
+		this.viewportX = x;
+		this.viewportY = y;
+		this.viewportWidth = w == 0 ? scene.width : w;
+		this.viewportHeight = h == 0 ? scene.height : h;
 	}
 	}
 
 
-	override private function sync( ctx : RenderContext )
-	{
-		if ( scene != null && (ratioChanged || scene.width != sceneWidth || scene.height != sceneHeight) ) {
-			var oldX = -this.x + anchorWidth;
-			var oldY = -this.y + anchorHeight;
-			this.sceneWidth = scene.width;
-			this.sceneHeight = scene.height;
-			this.width = Math.round(scene.width * horizontalRatio);
-			this.height = Math.round(scene.height * verticalRatio);
-			this.anchorWidth = width * anchorX;
-			this.anchorHeight = height * anchorY;
-			this.x = -oldX + anchorWidth;
-			this.y = -oldY + anchorHeight;
-			this.ratioChanged = false;
-		}
-		super.sync(ctx);
-	}
-
-	inline function initScene() {
-		this.scene = parent.getScene();
-		if ( scene != parent ) throw "Camera can be added only to Scene or MultiCamera!";
-		if ( sceneWidth == 0 )
-		{
-			sceneWidth = scene.width;
-			sceneHeight = scene.height;
-			anchorWidth = sceneWidth * anchorX;
-			anchorHeight = sceneHeight * anchorY;
-		}
+	/**
+		Sets camera viewport dimensions in raw format of 0..1 percentiles.
+	**/
+	public inline function setRawViewport( x : Float = 0, y : Float = 0, w : Float = 1, h : Float = 1 ) {
+		this.viewX = x;
+		this.viewY = y;
+		this.viewW = w;
+		this.viewH = h;
+		posChanged = true;
+	}
+
+	// Scren <-> Camera
+	/**
+		Convert screen position into a local camera position.
+		Requires Scene as a reference to viewport of `scaleMode`.
+	**/
+	inline function screenXToCamera( mx : Float, my : Float ) : Float {
+		return sceneXToCamera((mx - scene.offsetX) / scene.viewportScaleX, (my - scene.offsetY) / scene.viewportScaleY);
+	}
+
+	/**
+		Convert screen position into a local camera position.
+		Requires Scene as a reference to viewport of `scaleMode`.
+	**/
+	inline function screenYToCamera( mx : Float, my : Float ) : Float {
+		return sceneYToCamera((mx - scene.offsetX) / scene.viewportScaleX, (my - scene.offsetY) / scene.viewportScaleY);
+	}
+
+	/**
+		Convert local camera position to absolute screen position.
+		Requires Scene as a reference to viewport of `scaleMode`.
+	**/
+	inline function cameraXToScreen( mx : Float, my : Float ) : Float {
+		return cameraXToScene(mx, my) * scene.viewportScaleX + scene.offsetX;
+	}
+
+	/**
+		Convert local camera position to absolute screen position.
+		Requires Scene as a reference to viewport of `scaleMode`.
+	**/
+	inline function cameraYToScreen( mx : Float, my : Float ) : Float {
+		return cameraYToScene(mx, my) * scene.viewportScaleY + scene.offsetY;
+	}
+
+	// Scene <-> Camera
+	/**
+		Convert an absolute scene position into a local camera position.
+		Does not represent screen position, see `screenXToCamera` to convert position with accounting of `scaleMode`.
+	**/
+	inline function sceneXToCamera( mx : Float, my : Float ) : Float {
+		return ((mx - absX) * matD - (my - absY) * matC) * invDet;
+	}
+
+	/**
+		Convert an absolute scene position into a local camera position.
+		Does not represent screen position, see `screenYToCamera` to convert position with accounting of `scaleMode`.
+	**/
+	inline function sceneYToCamera( mx : Float, my : Float ) : Float {
+		return (-(mx - absX) * matB + (my - absY) * matA) * invDet;
+	}
+
+	/**
+		Convert local camera position into absolute scene position.
+		Does not represent screen position, see `cameraXToScreen` to convert position with accounting of `scaleMode`.
+	**/
+	inline function cameraXToScene( mx : Float, my : Float ) : Float {
+		return mx * matA + my * matC + absX;
+	}
+
+	/**
+		Convert local camera position into absolute scene position.
+		Does not represent screen position, see `cameraYToScreen` to convert position with accounting of `scaleMode`.
+	**/
+	inline function cameraYToScene( mx : Float, my : Float ) : Float {
+		return mx * matB + my * matD + absY;
+	}
+
+	// Point/event
+
+	@:noCompletion public function eventToCamera( e : hxd.Event ) {
+		var x = (e.relX - scene.offsetX) / scene.viewportScaleX - absX;
+		var y = (e.relY - scene.offsetY) / scene.viewportScaleY - absY;
+		e.relX = (x * matD - y * matC) * invDet;
+		e.relY = (-x * matB + y * matA) * invDet;
+	}
+
+	/**
+		Convert screen position into a local camera position.
+		Requires Scene as a reference to viewport of `scaleMode`.
+	**/
+	public function screenToCamera( pt : h2d.col.Point ) {
+		checkScene();
+		var x = (pt.x - scene.offsetX) / scene.viewportScaleX - absX;
+		var y = (pt.y - scene.offsetY) / scene.viewportScaleY - absY;
+		pt.x = (x * matD - y * matC) * invDet;
+		pt.y = (-x * matB + y * matA) * invDet;
+	}
+
+	/**
+		Convert local camera position to absolute screen position.
+		Requires Scene as a reference to viewport of `scaleMode`.
+	**/
+	public function cameraToScreen( pt : h2d.col.Point ) {
+		checkScene();
+		var x = pt.x;
+		var y = pt.y;
+		pt.x = cameraXToScreen(x, y);
+		pt.y = cameraYToScreen(x, y);
+	}
+
+	/**
+		Convert an absolute scene position into a local camera position.
+		Does not represent screen position, see `screenToCamera` to convert position with accounting of `scaleMode`.
+	**/
+	public function sceneToCamera( pt : h2d.col.Point ) {
+		checkScene();
+		var x = pt.x - absX;
+		var y = pt.y - absY;
+		pt.x = (x * matD - y * matC) * invDet;
+		pt.y = (-x * matB + y * matA) * invDet;
+	}
+
+	/**
+		Convert local camera position into absolute scene position.
+		Does not represent screen position, see `cameraToScreen` to convert position with accounting of `scaleMode`.
+	**/
+	public function cameraToScene( pt : h2d.col.Point ) {
+		checkScene();
+		var x = pt.x;
+		var y = pt.y;
+		pt.x = cameraXToScene(x, y);
+		pt.y = cameraYToScene(x, y);
+	}
+
+	inline function checkScene() {
+		if (scene == null) throw "This method requires Camera to be added to the Scene";
+	}
+
+	// Setters
+
+	inline function set_x( v ) {
+		posChanged = true;
+		return this.x = v;
+	}
+
+	inline function set_y( v ) {
+		posChanged = true;
+		return this.y = v;
+	}
+
+	inline function set_scaleX( v ) {
+		posChanged = true;
+		return this.scaleX = v;
+	}
+
+	inline function set_scaleY( v ) {
+		posChanged = true;
+		return this.scaleY = v;
+	}
+
+	inline function set_rotation( v ) {
+		posChanged = true;
+		return this.rotation = v;
+	}
+
+	inline function get_viewportX() { checkScene(); return viewX * scene.width; }
+	inline function set_viewportX( v ) {
+		checkScene();
+		posChanged = true;
+		this.viewX = Math.floor(v) / scene.width;
+		return v;
+	}
+
+	inline function get_viewportY() { checkScene(); return viewY * scene.height; }
+	inline function set_viewportY( v ) {
+		checkScene();
+		posChanged = true;
+		this.viewY = Math.floor(v) / scene.height;
+		return v;
+	}
+
+	inline function get_viewportWidth() { checkScene(); return viewW * scene.width; }
+	inline function set_viewportWidth( v ) {
+		checkScene();
+		posChanged = true;
+		this.viewW = Math.ceil(v) / scene.width;
+		return v;
+	}
+
+	inline function get_viewportHeight() { checkScene(); return viewH * scene.height; }
+	inline function set_viewportHeight( v ) {
+		checkScene();
+		posChanged = true;
+		this.viewH = Math.ceil(v) / scene.height;
+		return v;
+	}
+
+	inline function set_anchorX( v ) {
+		posChanged = true;
+		return anchorX = v;
+	}
+
+	inline function set_anchorY( v ) {
+		posChanged = true;
+		return anchorY = v;
 	}
 	}
 
 
 }
 }

+ 0 - 1
h2d/Layers.hx

@@ -189,5 +189,4 @@ class Layers extends Object {
 		}
 		}
 	}
 	}
 
 
-
 }
 }

+ 35 - 28
h2d/Object.hx

@@ -643,19 +643,25 @@ class Object #if (domkit && !domkit_heaps) implements domkit.Model<h2d.Object> #
 		@:privateAccess if( ctx.inFilter != null ) {
 		@:privateAccess if( ctx.inFilter != null ) {
 			var f1 = ctx.baseShader.filterMatrixA;
 			var f1 = ctx.baseShader.filterMatrixA;
 			var f2 = ctx.baseShader.filterMatrixB;
 			var f2 = ctx.baseShader.filterMatrixB;
-			matA = this.matA * f1.x + this.matB * f1.y;
-			matB = this.matA * f2.x + this.matB * f2.y;
-			matC = this.matC * f1.x + this.matD * f1.y;
-			matD = this.matC * f2.x + this.matD * f2.y;
-			absX = this.absX * f1.x + this.absY * f1.y + f1.z;
-			absY = this.absX * f2.x + this.absY * f2.y + f2.z;
+			var tmpA = this.matA * f1.x + this.matB * f1.y;
+			var tmpB = this.matA * f2.x + this.matB * f2.y;
+			var tmpC = this.matC * f1.x + this.matD * f1.y;
+			var tmpD = this.matC * f2.x + this.matD * f2.y;
+			var tmpX = this.absX * f1.x + this.absY * f1.y + f1.z;
+			var tmpY = this.absX * f2.x + this.absY * f2.y + f2.z;
+			matA = tmpA * ctx.viewA + tmpB * ctx.viewC;
+			matB = tmpA * ctx.viewB + tmpB * ctx.viewD;
+			matC = tmpC * ctx.viewA + tmpD * ctx.viewC;
+			matD = tmpC * ctx.viewB + tmpD * ctx.viewD;
+			absX = tmpX * ctx.viewA + tmpY * ctx.viewC + ctx.viewX;
+			absY = tmpX * ctx.viewB + tmpY * ctx.viewD + ctx.viewY;
 		} else {
 		} else {
-			matA = this.matA;
-			matB = this.matB;
-			matC = this.matC;
-			matD = this.matD;
-			absX = this.absX;
-			absY = this.absY;
+			matA = this.matA * ctx.viewA + this.matB * ctx.viewC;
+			matB = this.matA * ctx.viewB + this.matB * ctx.viewD;
+			matC = this.matC * ctx.viewA + this.matD * ctx.viewC;
+			matD = this.matC * ctx.viewB + this.matD * ctx.viewD;
+			absX = this.absX * ctx.viewA + this.absY * ctx.viewC + ctx.viewX;
+			absY = this.absX * ctx.viewB + this.absY * ctx.viewD + ctx.viewY;
 		}
 		}
 
 
 		// intersect our transformed local view with our viewport in global space
 		// intersect our transformed local view with our viewport in global space
@@ -670,10 +676,10 @@ class Object #if (domkit && !domkit_heaps) implements domkit.Model<h2d.Object> #
 
 
 		// clip with our scene
 		// clip with our scene
 		@:privateAccess {
 		@:privateAccess {
-			if( view.xMin < ctx.curX ) view.xMin = ctx.curX;
-			if( view.yMin < ctx.curY ) view.yMin = ctx.curY;
-			if( view.xMax > ctx.curX + ctx.curWidth ) view.xMax = ctx.curX + ctx.curWidth;
-			if( view.yMax > ctx.curY + ctx.curHeight ) view.yMax = ctx.curY + ctx.curHeight;
+			if( view.xMin < -1 ) view.xMin = -1;
+			if( view.yMin < -1 ) view.yMin = -1;
+			if( view.xMax > 1 ) view.xMax = 1;
+			if( view.yMax > 1 ) view.yMax = 1;
 		}
 		}
 
 
 		// inverse our matrix
 		// inverse our matrix
@@ -742,7 +748,6 @@ class Object #if (domkit && !domkit_heaps) implements domkit.Model<h2d.Object> #
 		var shader = @:privateAccess ctx.baseShader;
 		var shader = @:privateAccess ctx.baseShader;
 		var oldA = shader.filterMatrixA.clone();
 		var oldA = shader.filterMatrixA.clone();
 		var oldB = shader.filterMatrixB.clone();
 		var oldB = shader.filterMatrixB.clone();
-		var oldF = @:privateAccess ctx.inFilter;
 
 
 		// 2x3 inverse matrix
 		// 2x3 inverse matrix
 		var invDet = 1 / (matA * matD - matB * matC);
 		var invDet = 1 / (matA * matD - matB * matC);
@@ -756,9 +761,7 @@ class Object #if (domkit && !domkit_heaps) implements domkit.Model<h2d.Object> #
 		shader.filterMatrixA.set(invA, invC, invX);
 		shader.filterMatrixA.set(invA, invC, invX);
 		shader.filterMatrixB.set(invB, invD, invY);
 		shader.filterMatrixB.set(invB, invD, invY);
 		ctx.globalAlpha = 1;
 		ctx.globalAlpha = 1;
-		draw(ctx);
-		for( c in children )
-			c.drawRec(ctx);
+		drawContent(ctx);
 		ctx.flush();
 		ctx.flush();
 
 
 		var finalTile = h2d.Tile.fromTexture(t);
 		var finalTile = h2d.Tile.fromTexture(t);
@@ -815,18 +818,22 @@ class Object #if (domkit && !domkit_heaps) implements domkit.Model<h2d.Object> #
 		} else {
 		} else {
 			var old = ctx.globalAlpha;
 			var old = ctx.globalAlpha;
 			ctx.globalAlpha *= alpha;
 			ctx.globalAlpha *= alpha;
-			if( ctx.front2back ) {
-				var nchilds = children.length;
-				for (i in 0...nchilds) children[nchilds - 1 - i].drawRec(ctx);
-				draw(ctx);
-			} else {
-				draw(ctx);
-				for( c in children ) c.drawRec(ctx);
-			}
+			drawContent(ctx);
 			ctx.globalAlpha = old;
 			ctx.globalAlpha = old;
 		}
 		}
 	}
 	}
 
 
+	function drawContent( ctx : RenderContext ) {
+		if ( ctx.front2back ) {
+			var i = children.length;
+			while ( i-- > 0 ) children[i].drawRec(ctx);
+			draw(ctx);
+		} else {
+			draw(ctx);
+			for ( c in children ) c.drawRec(ctx);
+		}
+	}
+
 	inline function set_x(v) {
 	inline function set_x(v) {
 		posChanged = true;
 		posChanged = true;
 		return x = v;
 		return x = v;

+ 153 - 91
h2d/RenderContext.hx

@@ -1,7 +1,15 @@
 package h2d;
 package h2d;
 
 
+private typedef CameraStackEntry = {
+	va : Float, vb : Float, vc : Float, vd : Float, vx : Float, vy : Float
+};
+private typedef TargetStackEntry = CameraStackEntry & {
+	t : h3d.mat.Texture, hasRZ : Bool, rzX:Float, rzY:Float, rzW:Float, rzH:Float
+};
+
 private typedef RenderZoneStack = { hasRZ:Bool, x:Float, y:Float, w:Float, h:Float };
 private typedef RenderZoneStack = { hasRZ:Bool, x:Float, y:Float, w:Float, h:Float };
 
 
+@:access(h2d.Scene)
 class RenderContext extends h3d.impl.RenderContext {
 class RenderContext extends h3d.impl.RenderContext {
 
 
 	static inline var BUFFERING = false;
 	static inline var BUFFERING = false;
@@ -30,8 +38,11 @@ class RenderContext extends h3d.impl.RenderContext {
 	var baseShaderList : hxsl.ShaderList;
 	var baseShaderList : hxsl.ShaderList;
 	var currentObj : Drawable;
 	var currentObj : Drawable;
 	var stride : Int;
 	var stride : Int;
-	var targetsStack : Array<{ t : h3d.mat.Texture, x : Int, y : Int, w : Int, h : Int, hasRZ : Bool, rzX:Float, rzY:Float, rzW:Float, rzH:Float }>;
+	var targetsStack : Array<TargetStackEntry>;
 	var targetsStackIndex : Int;
 	var targetsStackIndex : Int;
+	var cameraStack : Array<CameraStackEntry>;
+	var cameraStackIndex : Int;
+	var curTarget : h3d.mat.Texture;
 	var renderZoneStack:Array<RenderZoneStack> = [];
 	var renderZoneStack:Array<RenderZoneStack> = [];
 	var renderZoneIndex:Int = 0;
 	var renderZoneIndex:Int = 0;
 	var hasUVPos : Bool;
 	var hasUVPos : Bool;
@@ -39,10 +50,12 @@ class RenderContext extends h3d.impl.RenderContext {
 	var inFilter : Object;
 	var inFilter : Object;
 	var inFilterBlend : BlendMode;
 	var inFilterBlend : BlendMode;
 
 
-	var curX : Int;
-	var curY : Int;
-	var curWidth : Int;
-	var curHeight : Int;
+	var viewA : Float;
+	var viewB : Float;
+	var viewC : Float;
+	var viewD : Float;
+	var viewX : Float;
+	var viewY : Float;
 
 
 	var hasRenderZone : Bool;
 	var hasRenderZone : Bool;
 	var renderX : Float;
 	var renderX : Float;
@@ -69,6 +82,8 @@ class RenderContext extends h3d.impl.RenderContext {
 		baseShaderList = new hxsl.ShaderList(baseShader);
 		baseShaderList = new hxsl.ShaderList(baseShader);
 		targetsStack = [];
 		targetsStack = [];
 		targetsStackIndex = 0;
 		targetsStackIndex = 0;
+		cameraStack = [];
+		cameraStackIndex = 0;
 		filterStack = [];
 		filterStack = [];
 	}
 	}
 
 
@@ -86,19 +101,23 @@ class RenderContext extends h3d.impl.RenderContext {
 		currentObj = null;
 		currentObj = null;
 		bufPos = 0;
 		bufPos = 0;
 		stride = 0;
 		stride = 0;
-		curX = 0;
-		curY = 0;
+		viewA = scene.viewportA;
+		viewB = 0;
+		viewC = 0;
+		viewD = scene.viewportD;
+		viewX = scene.viewportX;
+		viewY = scene.viewportY;
+
 		targetFlipY = engine.driver.hasFeature(BottomLeftCoords) ? -1 : 1;
 		targetFlipY = engine.driver.hasFeature(BottomLeftCoords) ? -1 : 1;
 		baseFlipY = engine.getCurrentTarget() != null ? targetFlipY : 1;
 		baseFlipY = engine.getCurrentTarget() != null ? targetFlipY : 1;
 		inFilter = null;
 		inFilter = null;
-		curWidth = scene.width;
-		curHeight = scene.height;
 		manager.globals.set("time", time);
 		manager.globals.set("time", time);
 		manager.globals.set("global.time", time);
 		manager.globals.set("global.time", time);
 		// todo : we might prefer to auto-detect this by running a test and capturing its output
 		// todo : we might prefer to auto-detect this by running a test and capturing its output
 		baseShader.pixelAlign = #if flash true #else false #end;
 		baseShader.pixelAlign = #if flash true #else false #end;
 		baseShader.halfPixelInverse.set(0.5 / engine.width, 0.5 / engine.height);
 		baseShader.halfPixelInverse.set(0.5 / engine.width, 0.5 / engine.height);
-		baseShader.viewport.set( -scene.width * 0.5 - scene.offsetX, -scene.height * 0.5 - scene.offsetY, 2 / scene.width * scene.ratioX, -2 * baseFlipY / scene.height * scene.ratioY);
+		baseShader.viewportA.set(scene.viewportA, 0, scene.viewportX);
+		baseShader.viewportB.set(0, scene.viewportD * -baseFlipY, scene.viewportY * -baseFlipY);
 		baseShader.filterMatrixA.set(1, 0, 0);
 		baseShader.filterMatrixA.set(1, 0, 0);
 		baseShader.filterMatrixB.set(0, 1, 0);
 		baseShader.filterMatrixB.set(0, 1, 0);
 		baseShaderList.next = null;
 		baseShaderList.next = null;
@@ -134,7 +153,52 @@ class RenderContext extends h3d.impl.RenderContext {
 		texture = null;
 		texture = null;
 		currentObj = null;
 		currentObj = null;
 		baseShaderList.next = null;
 		baseShaderList.next = null;
-		if( targetsStackIndex != 0 ) throw "Missing popTarget()";
+		if ( targetsStackIndex != 0 ) throw "Missing popTarget()";
+		if ( cameraStackIndex != 0 ) throw "Missing popCamera()";
+	}
+
+	@:access(h2d.Camera)
+	public function pushCamera( cam : h2d.Camera ) {
+		var entry = cameraStack[cameraStackIndex++];
+		if ( entry == null ) {
+			entry = { va: 0, vb: 0, vc: 0, vd: 0, vx: 0, vy: 0 };
+			cameraStack.push(entry);
+		}
+		var tmpA = viewA;
+		var tmpB = viewB;
+		var tmpC = viewC;
+		var tmpD = viewD;
+
+		entry.va = tmpA;
+		entry.vb = tmpB;
+		entry.vc = tmpC;
+		entry.vd = tmpD;
+		entry.vx = viewX;
+		entry.vy = viewY;
+		
+		viewA = cam.matA * tmpA + cam.matB * tmpC;
+		viewB = cam.matA * tmpB + cam.matB * tmpD;
+		viewC = cam.matC * tmpA + cam.matD * tmpC;
+		viewD = cam.matC * tmpB + cam.matD * tmpD;
+		viewX = cam.absX * tmpA + cam.absY * tmpC + viewX;
+		viewY = cam.absX * tmpB + cam.absY * tmpD + viewY;
+		var flipY = curTarget != null ? -targetFlipY : -baseFlipY;
+		baseShader.viewportA.set(viewA, viewC, viewX);
+		baseShader.viewportB.set(viewB * flipY, viewD * flipY, viewY * flipY);
+	}
+
+	public function popCamera() {
+		if (cameraStackIndex == 0) throw "Too many popCamera()";
+		var inf = cameraStack[--cameraStackIndex];
+		viewA = inf.va;
+		viewB = inf.vb;
+		viewC = inf.vc;
+		viewD = inf.vd;
+		viewX = inf.vx;
+		viewY = inf.vy;
+		var flipY = curTarget != null ? -targetFlipY : -baseFlipY;
+		baseShader.viewportA.set(viewA, viewC, viewX);
+		baseShader.viewportB.set(viewB * flipY, viewD * flipY, viewY * flipY);
 	}
 	}
 
 
 	public function pushFilter( spr : h2d.Object ) {
 	public function pushFilter( spr : h2d.Object ) {
@@ -159,30 +223,39 @@ class RenderContext extends h3d.impl.RenderContext {
 		flush();
 		flush();
 		engine.pushTarget(t);
 		engine.pushTarget(t);
 		initShaders(baseShaderList);
 		initShaders(baseShaderList);
+		
+		var entry = targetsStack[targetsStackIndex++];
+		if ( entry == null ) {
+			entry = { t: null, va: 0, vb: 0, vc: 0, vd: 0, vx: 0, vy: 0, hasRZ: false, rzX: 0, rzY: 0, rzW: 0, rzH: 0 };
+			targetsStack.push(entry);
+		}
+		entry.t = curTarget;
+		entry.va = viewA;
+		entry.vb = viewB;
+		entry.vc = viewC;
+		entry.vd = viewD;
+		entry.vx = viewX;
+		entry.vy = viewY;
+		entry.hasRZ = hasRenderZone;
+		entry.rzX = renderX;
+		entry.rzY = renderY;
+		entry.rzW = renderW;
+		entry.rzH = renderH;
+
 		if( width < 0 ) width = t == null ? scene.width : t.width;
 		if( width < 0 ) width = t == null ? scene.width : t.width;
 		if( height < 0 ) height = t == null ? scene.height : t.height;
 		if( height < 0 ) height = t == null ? scene.height : t.height;
+
+		viewA = 2 / width;
+		viewB = 0;
+		viewC = 0;
+		viewD = 2 / height;
+		viewX = -1 - startX * viewA;
+		viewY = -1 - startY * viewD;
+
 		baseShader.halfPixelInverse.set(0.5 / (t == null ? engine.width : t.width), 0.5 / (t == null ? engine.height : t.height));
 		baseShader.halfPixelInverse.set(0.5 / (t == null ? engine.width : t.width), 0.5 / (t == null ? engine.height : t.height));
-		baseShader.viewport.set( -width * 0.5 - startX, -height * 0.5 - startY, 2 / width, -2 * targetFlipY / height);
-		targetsStackIndex++;
-		if( targetsStackIndex > targetsStack.length ){
-			targetsStack.push( { t : t, x : startX, y : startY, w : width, h : height, hasRZ: hasRenderZone, rzX: renderX, rzY:renderY, rzW:renderW, rzH:renderH } );
-		}else{
-			var o = targetsStack[targetsStackIndex-1];
-			o.t = t;
-			o.x = startX;
-			o.y = startY;
-			o.w = width;
-			o.h = height;
-			o.hasRZ = hasRenderZone;
-			o.rzX = renderX;
-			o.rzY = renderY;
-			o.rzW = renderW;
-			o.rzH = renderH;
-		}
-		curX = startX;
-		curY = startY;
-		curWidth = width;
-		curHeight = height;
+		baseShader.viewportA.set(viewA, viewC, viewX);
+		baseShader.viewportB.set(viewB * -targetFlipY, viewD * -targetFlipY, viewY * -targetFlipY);
+		curTarget = t;
 		currentBlend = null;
 		currentBlend = null;
 		if( hasRenderZone ) clearRZ();
 		if( hasRenderZone ) clearRZ();
 	}
 	}
@@ -195,48 +268,27 @@ class RenderContext extends h3d.impl.RenderContext {
 		}
 		}
 	}
 	}
 
 
-	public function popTarget( restore = true ) {
+	public function popTarget() {
 		flush();
 		flush();
 		if( targetsStackIndex <= 0 ) throw "Too many popTarget()";
 		if( targetsStackIndex <= 0 ) throw "Too many popTarget()";
-		var pinf = targetsStack[--targetsStackIndex];
 		engine.popTarget();
 		engine.popTarget();
 
 
-		if( restore ) {
-			var tinf = targetsStack[targetsStackIndex - 1];
-			var t : h3d.mat.Texture;
-			var startX : Int, startY : Int, width : Int, height : Int;
-			var ratioX : Float, ratioY : Float, offsetX : Float, offsetY : Float;
-			if ( tinf == null ) {
-				t = null;
-				startX = 0;
-				startY = 0;
-				width = scene.width;
-				height = scene.height;
-				ratioX = scene.ratioX;
-				ratioY = scene.ratioY;
-				offsetX = scene.offsetX;
-				offsetY = scene.offsetY;
-			} else {
-				t = tinf.t;
-				startX = tinf.x;
-				startY = tinf.y;
-				width = tinf.w;
-				height = tinf.h;
-				ratioX = 1;
-				ratioY = 1;
-				offsetX = 0;
-				offsetY = 0;
-			}
-			initShaders(baseShaderList);
-			baseShader.halfPixelInverse.set(0.5 / (t == null ? engine.width : t.width), 0.5 / (t == null ? engine.height : t.height));
-			baseShader.viewport.set( -width * 0.5 - startX - offsetX, -height * 0.5 - startY - offsetY, 2 / width * ratioX, -2 * (t == null ? baseFlipY : targetFlipY) / height * ratioY);
-			curX = startX;
-			curY = startY;
-			curWidth = width;
-			curHeight = height;
-		}
+		var tinf = targetsStack[--targetsStackIndex];
+		var t : h3d.mat.Texture = curTarget = tinf.t;
+		viewA = tinf.va;
+		viewB = tinf.vb;
+		viewC = tinf.vc;
+		viewD = tinf.vd;
+		viewX = tinf.vx;
+		viewY = tinf.vy;
+		var flipY = t == null ? -baseFlipY : -targetFlipY;
+
+		initShaders(baseShaderList);
+		baseShader.halfPixelInverse.set(0.5 / (t == null ? engine.width : t.width), 0.5 / (t == null ? engine.height : t.height));
+		baseShader.viewportA.set(viewA, viewC, viewX);
+		baseShader.viewportB.set(viewB * flipY, viewD * flipY, viewY * flipY);
 
 
-		if( pinf.hasRZ ) setRZ(pinf.rzX, pinf.rzY, pinf.rzW, pinf.rzH);
+		if ( tinf.hasRZ ) setRZ(tinf.rzX, tinf.rzY, tinf.rzW, tinf.rzH);
 	}
 	}
 
 
 	public function pushRenderZone( x : Float, y : Float, w : Float, h : Float ) {
 	public function pushRenderZone( x : Float, y : Float, w : Float, h : Float ) {
@@ -293,8 +345,8 @@ class RenderContext extends h3d.impl.RenderContext {
 		renderY = y;
 		renderY = y;
 		renderW = w;
 		renderW = w;
 		renderH = h;
 		renderH = h;
-		var scaleX = engine.width * scene.ratioX / scene.width;
-		var scaleY = engine.height * scene.ratioY / scene.height;
+		var scaleX = scene.viewportA * engine.width / 2;
+		var scaleY = scene.viewportD * engine.height / 2;
 		if( inFilter != null ) {
 		if( inFilter != null ) {
 			var fa = baseShader.filterMatrixA;
 			var fa = baseShader.filterMatrixA;
 			var fb = baseShader.filterMatrixB;
 			var fb = baseShader.filterMatrixB;
@@ -308,7 +360,13 @@ class RenderContext extends h3d.impl.RenderContext {
 			w = rx2 - rx1;
 			w = rx2 - rx1;
 			h = ry2 - ry1;
 			h = ry2 - ry1;
 		}
 		}
-		engine.setRenderZone(Std.int((x - curX + scene.viewportX) * scaleX + 1e-10), Std.int((y - curY + scene.viewportY) * scaleY + 1e-10), Std.int(w * scaleX + 1e-10), Std.int(h * scaleY + 1e-10));
+		
+		engine.setRenderZone(
+			Std.int(x * scaleX + (scene.viewportX+1) * (engine.width / 2) + 1e-10),
+			Std.int(y * scaleY + (scene.viewportY+1) * (engine.height / 2) + 1e-10),
+			Std.int(w * scaleX + 1e-10),
+			Std.int(h * scaleY + 1e-10)
+		);
 	}
 	}
 	
 	
 	inline function clearRZ() {
 	inline function clearRZ() {
@@ -412,29 +470,35 @@ class RenderContext extends h3d.impl.RenderContext {
 		if( inFilter != null ) {
 		if( inFilter != null ) {
 			var f1 = baseShader.filterMatrixA;
 			var f1 = baseShader.filterMatrixA;
 			var f2 = baseShader.filterMatrixB;
 			var f2 = baseShader.filterMatrixB;
-			matA = obj.matA * f1.x + obj.matB * f1.y;
-			matB = obj.matA * f2.x + obj.matB * f2.y;
-			matC = obj.matC * f1.x + obj.matD * f1.y;
-			matD = obj.matC * f2.x + obj.matD * f2.y;
-			absX = obj.absX * f1.x + obj.absY * f1.y + f1.z;
-			absY = obj.absX * f2.x + obj.absY * f2.y + f2.z;
+			var tmpA = obj.matA * f1.x + obj.matB * f1.y;
+			var tmpB = obj.matA * f2.x + obj.matB * f2.y;
+			var tmpC = obj.matC * f1.x + obj.matD * f1.y;
+			var tmpD = obj.matC * f2.x + obj.matD * f2.y;
+			var tmpX = obj.absX * f1.x + obj.absY * f1.y + f1.z;
+			var tmpY = obj.absX * f2.x + obj.absY * f2.y + f2.z;
+			matA = tmpA * viewA + tmpB * viewC;
+			matB = tmpA * viewB + tmpB * viewD;
+			matC = tmpC * viewA + tmpD * viewC;
+			matD = tmpC * viewB + tmpD * viewD;
+			absX = tmpX * viewA + tmpY * viewC + viewX;
+			absY = tmpX * viewB + tmpY * viewD + viewY;
 		} else {
 		} else {
-			matA = obj.matA;
-			matB = obj.matB;
-			matC = obj.matC;
-			matD = obj.matD;
-			absX = obj.absX;
-			absY = obj.absY;
+			matA = obj.matA * viewA + obj.matB * viewC;
+			matB = obj.matA * viewB + obj.matB * viewD;
+			matC = obj.matC * viewA + obj.matD * viewC;
+			matD = obj.matC * viewB + obj.matD * viewD;
+			absX = obj.absX * viewA + obj.absY * viewC + viewX;
+			absY = obj.absX * viewB + obj.absY * viewD + viewY;
 		}
 		}
 
 
 		// check if our tile is outside of the viewport
 		// check if our tile is outside of the viewport
 		if( matB == 0 && matC == 0 ) {
 		if( matB == 0 && matC == 0 ) {
 			var tx = tile.dx + tile.width * 0.5;
 			var tx = tile.dx + tile.width * 0.5;
 			var ty = tile.dy + tile.height * 0.5;
 			var ty = tile.dy + tile.height * 0.5;
-			var tr = (tile.width > tile.height ? tile.width : tile.height) * 1.5 * hxd.Math.max(hxd.Math.abs(obj.matA),hxd.Math.abs(obj.matD));
-			var cx = absX + tx * matA - curX;
-			var cy = absY + ty * matD - curY;
-			if( cx < -tr || cy < -tr || cx - tr > curWidth || cy - tr > curHeight ) return false;
+			var tr = (tile.width > tile.height ? tile.width : tile.height) * 1.5 * hxd.Math.max(hxd.Math.abs(matA),hxd.Math.abs(matD));
+			var cx = absX + tx * matA;
+			var cy = absY + ty * matD;
+			if ( cx + tr < -1 || cx - tr > 1 || cy + tr < -1 || cy - tr > 1) return false;
 		} else {
 		} else {
 			var xMin = 1e20, yMin = 1e20, xMax = -1e20, yMax = -1e20;
 			var xMin = 1e20, yMin = 1e20, xMax = -1e20, yMax = -1e20;
 			inline function calc(x:Float, y:Float) {
 			inline function calc(x:Float, y:Float) {
@@ -451,9 +515,7 @@ class RenderContext extends h3d.impl.RenderContext {
 			calc(tile.width, 0);
 			calc(tile.width, 0);
 			calc(0, tile.height);
 			calc(0, tile.height);
 			calc(tile.width, tile.height);
 			calc(tile.width, tile.height);
-			var cx = absX - curX;
-			var cy = absY - curY;
-			if( cx + xMax < 0 || cy + yMax < 0 || cx + xMin > curWidth || cy + yMin > curHeight )
+			if (absX + xMax < -1 || absY + yMax < -1 || absX + xMin > 1 || absY + yMin > 1)
 				return false;
 				return false;
 		}
 		}
 
 

+ 184 - 64
h2d/Scene.hx

@@ -82,35 +82,46 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 	**/
 	**/
 	public var height(default, null) : Int;
 	public var height(default, null) : Int;
 
 
+	/**
+		Viewport horizontal scale transform value. Converts from scene space to screen space of [0, 2] range.
+	**/
+	var viewportA(default, null) : Float;
+	/**
+		Viewport vertical scale transform value. Converts from scene space to screen space of [0, 2] range.
+	**/
+	var viewportD(default, null) : Float;
 	/**
 	/**
 		Horizontal viewport offset relative to top-left corner of the window. Can change if the screen gets resized or `scaleMode` changes.
 		Horizontal viewport offset relative to top-left corner of the window. Can change if the screen gets resized or `scaleMode` changes.
-		Offset is in internal Scene resolution pixels.
+		Offset is in screen-space coordinates: [-1, 1] where 0 is center of the window.
 	**/
 	**/
-	public var viewportX(default, null) : Float;
+	var viewportX(default, null) : Float;
 	/**
 	/**
 		Vertical viewport offset relative to top-left corner of the window. Can change if the screen gets resized or `scaleMode` changes.
 		Vertical viewport offset relative to top-left corner of the window. Can change if the screen gets resized or `scaleMode` changes.
-		Offset is in internal Scene resolution pixels.
+		Offset is in screen-space coordinates: [-1, 1] where 0 is center of the window.
 	**/
 	**/
-	public var viewportY(default, null) : Float;
+	var viewportY(default, null) : Float;
+
 	/**
 	/**
-		Physical vertical viewport offset relative to the center of the window. Assigned if the screen gets resized or `scaleMode` changes.
-		Offset is in internal Scene resolution pixels.
+		Horizontal viewport offset relative to top-left corner of the window in pixels.
+		Assigned if the screen gets resized or `scaleMode` changes.
 	**/
 	**/
-	public var offsetX : Float;
+	var offsetX(default, null) : Float;
 	/**
 	/**
-		Physical horizontal viewport offset relative to the center of the window. Assigned if the screen gets resized or `scaleMode` changes.
-		Offset is in internal Scene resolution pixels.
+		Vertical viewport offset relative to top-left corner of the window in pixels.
+		Assigned if the screen gets resized or `scaleMode` changes.
 	**/
 	**/
-	public var offsetY : Float;
+	var offsetY(default, null) : Float;
 
 
 	/**
 	/**
-		Horizontal ratio of the window used by the Scene (including scaling). Can change if the screen gets resized or `scaleMode` changes.
+		Horizontal scale of a scene when rendering to screen.
+		Can change if the screen gets resized or `scaleMode` changes.
 	**/
 	**/
-	public var ratioX(default, null) : Float;
+	public var viewportScaleX(default, null) : Float;
 	/**
 	/**
-		Vertical ratio of the window used by the Scene (including scaling). Can change if the screen gets resized or `scaleMode` changes.
+		Vertical scale of a scene when rendering to screen.
+		Can change if the screen gets resized or `scaleMode` changes.
 	**/
 	**/
-	public var ratioY(default, null) : Float;
+	public var viewportScaleY(default, null) : Float;
 
 
 	/**
 	/**
 		The current mouse X coordinates (in pixel) relative to the scene.
 		The current mouse X coordinates (in pixel) relative to the scene.
@@ -137,6 +148,26 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 	**/
 	**/
 	public var scaleMode(default, set) : ScaleMode = Resize;
 	public var scaleMode(default, set) : ScaleMode = Resize;
 
 
+	/**
+		List of all cameras attached to the Scene. Should contain at least one camera to render (created by default).
+		Override `h2d.Camera.layerVisible` method to filter out specific layers from camera rendering.
+		To add or remove cameras use `addCamera` and `removeCamera` methods.
+	**/
+	public var cameras(get, never) : haxe.ds.ReadOnlyArray<Camera>;
+	var _cameras : Array<Camera>;
+	/**
+		Alias to first camera in the list: `cameras[0]`
+	**/
+	public var camera(get, never) : Camera;
+
+	/**
+		Camera instance that handles scene events.
+		Due to Heaps structure, only one Camera can work with the Interactives.
+		Contrary to rendering, event handling does not check if layer is visible for camera or not.
+		Should never be null. If Camera does not belong to the Scene, it will be added with `Scene.addCamera`.
+	**/
+	public var interactiveCamera(default, set) : Camera;
+
 	/**
 	/**
 		Set the default value for `h2d.Drawable.smooth` (default: false)
 		Set the default value for `h2d.Drawable.smooth` (default: false)
 	**/
 	**/
@@ -162,14 +193,19 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 		super(null);
 		super(null);
 		var e = h3d.Engine.getCurrent();
 		var e = h3d.Engine.getCurrent();
 		ctx = new RenderContext(this);
 		ctx = new RenderContext(this);
+		_cameras = [];
+		new Camera(this);
+		interactiveCamera = camera;
 		width = e.width;
 		width = e.width;
 		height = e.height;
 		height = e.height;
+		viewportA = 2 / e.width;
+		viewportD = 2 / e.height;
+		viewportX = -1;
+		viewportY = -1;
+		viewportScaleX = 1;
+		viewportScaleY = 1;
 		offsetX = 0;
 		offsetX = 0;
 		offsetY = 0;
 		offsetY = 0;
-		ratioX = 1;
-		ratioY = 1;
-		viewportX = 0;
-		viewportY = 0;
 		interactive = new Array();
 		interactive = new Array();
 		eventListeners = new Array();
 		eventListeners = new Array();
 		shapePoint = new h2d.col.Point();
 		shapePoint = new h2d.col.Point();
@@ -206,6 +242,33 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 	function get_renderer() return ctx;
 	function get_renderer() return ctx;
 	function set_renderer(v) { ctx = v; return v; }
 	function set_renderer(v) { ctx = v; return v; }
 
 
+	inline function get_camera() return _cameras[0];
+
+	inline function get_cameras() return _cameras;
+
+	function set_interactiveCamera( cam : Camera ) {
+		if ( cam == null ) throw "Interactive cammera cannot be null!";
+		if ( cam.scene != this ) this.addCamera(cam);
+		return interactiveCamera = cam;
+	}
+
+	/** Adds Camera to Scene camera list with optional index at which it is added. **/
+	public function addCamera( cam : Camera, ?pos : Int ) {
+		if ( cam.scene != null )
+			cam.scene.removeCamera(cam);
+		cam.scene = this;
+		cam.posChanged = true;
+		if ( pos != null ) _cameras.insert(pos, cam);
+		else _cameras.push(cam);
+	}
+
+	/** Removes Camera from Scene camera list. Current `interactiveCamera` cannot be removed. **/
+	public function removeCamera( cam : Camera ) {
+		if ( cam == interactiveCamera ) throw "Current interactive Camera cannot be removed from camera list!";
+		cam.scene = null;
+		_cameras.remove(cam);
+	}
+
 	/**
 	/**
 		Set the fixed size for the scene, will prevent automatic scene resizing when screen size changes.
 		Set the fixed size for the scene, will prevent automatic scene resizing when screen size changes.
 	**/
 	**/
@@ -226,56 +289,58 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 			}
 			}
 		}
 		}
 
 
-		inline function calcRatio( scale : Float ) {
-			ratioX = (width * scale) / engine.width;
-			ratioY = (height * scale) / engine.height;
+		inline function setViewportScale( sx : Float, sy : Float ) {
+			viewportScaleX = sx;
+			viewportScaleY = sy;
 		}
 		}
 
 
 		inline function calcViewport( horizontal : ScaleModeAlign, vertical : ScaleModeAlign, zoom : Float ) {
 		inline function calcViewport( horizontal : ScaleModeAlign, vertical : ScaleModeAlign, zoom : Float ) {
+			viewportA = (zoom * 2) / engine.width;
+			viewportD = (zoom * 2) / engine.height;
+			setViewportScale(zoom, zoom);
 			if ( horizontal == null ) horizontal = Center;
 			if ( horizontal == null ) horizontal = Center;
 			switch ( horizontal ) {
 			switch ( horizontal ) {
 				case Left:
 				case Left:
-					offsetX = (engine.width - width * zoom) / (2 * zoom);
-					viewportX = 0;
+					viewportX = -1;
+					offsetX = 0;
 				case Right:
 				case Right:
-					offsetX = -((engine.width - width * zoom) / (2 * zoom));
-					viewportX = (engine.width - width * zoom) / zoom;
+					viewportX = 1 - (width * viewportA);
+					offsetX = engine.width - width * zoom;
 				default:
 				default:
-					offsetX = -(((engine.width - width * zoom) / 2) % 1.)*.5;
-					viewportX = (engine.width - width * zoom) / (2 * zoom);
+					// Simple `width * viewportA - 0.5` causes gaps between tiles
+					viewportX = Math.floor((engine.width - width * zoom) / (zoom * 2)) * viewportA - 1.;
+					offsetX = Math.floor((engine.width - width * zoom) / 2);
 			}
 			}
 
 
 			if ( vertical == null ) vertical = Center;
 			if ( vertical == null ) vertical = Center;
 			switch ( vertical ) {
 			switch ( vertical ) {
 				case Top:
 				case Top:
-					offsetY = (engine.height - height * zoom) / (2 * zoom);
-					viewportY = 0;
+					viewportY = -1;
+					offsetY = 0;
 				case Bottom:
 				case Bottom:
-					offsetY = -((engine.height - height * zoom) / (2 * zoom));
-					viewportY = (engine.height - height * zoom) / zoom;
+					viewportY = 1 - (height * viewportD);
+					offsetY = engine.height - height * zoom;
 				default:
 				default:
-					offsetY = -(((engine.height - height * zoom) / 2) % 1.)*.5;
-					viewportY = (engine.height - height * zoom) / (2 * zoom);
+					viewportY = Math.floor((engine.height - height * zoom) / (zoom * 2)) * viewportD - 1.;
+					offsetY = Math.floor((engine.height - height * zoom) / 2);
 			}
 			}
 		}
 		}
 
 
 		inline function zeroViewport() {
 		inline function zeroViewport() {
-			offsetX = 0;
-			offsetY = 0;
-			viewportX = 0;
-			viewportY = 0;
+			viewportA = 2 / width;
+			viewportD = 2 / height;
+			viewportX = -1;
+			viewportY = -1;
 		}
 		}
 
 
 		switch ( scaleMode ) {
 		switch ( scaleMode ) {
 			case Resize:
 			case Resize:
 				setSceneSize(engine.width, engine.height);
 				setSceneSize(engine.width, engine.height);
-				ratioX = 1;
-				ratioY = 1;
+				setViewportScale(1, 1);
 				zeroViewport();
 				zeroViewport();
 			case Stretch(_width, _height):
 			case Stretch(_width, _height):
 				setSceneSize(_width, _height);
 				setSceneSize(_width, _height);
-				ratioX = 1;
-				ratioY = 1;
+				setViewportScale(engine.width / _width, engine.height / _height);
 				zeroViewport();
 				zeroViewport();
 			case LetterBox(_width, _height, integerScale, horizontalAlign, verticalAlign):
 			case LetterBox(_width, _height, integerScale, horizontalAlign, verticalAlign):
 				setSceneSize(_width, _height);
 				setSceneSize(_width, _height);
@@ -284,15 +349,13 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 					zoom = Std.int(zoom);
 					zoom = Std.int(zoom);
 					if (zoom == 0) zoom = 1;
 					if (zoom == 0) zoom = 1;
 				}
 				}
-				calcRatio(zoom);
 				calcViewport(horizontalAlign, verticalAlign, zoom);
 				calcViewport(horizontalAlign, verticalAlign, zoom);
 			case Fixed(_width, _height, zoom, horizontalAlign, verticalAlign):
 			case Fixed(_width, _height, zoom, horizontalAlign, verticalAlign):
 				setSceneSize(_width, _height);
 				setSceneSize(_width, _height);
-				calcRatio(zoom);
 				calcViewport(horizontalAlign, verticalAlign, zoom);
 				calcViewport(horizontalAlign, verticalAlign, zoom);
 			case Zoom(level):
 			case Zoom(level):
 				setSceneSize(Math.ceil(engine.width / level), Math.ceil(engine.height / level));
 				setSceneSize(Math.ceil(engine.width / level), Math.ceil(engine.height / level));
-				calcRatio(level);
+				setViewportScale(level, level);
 				zeroViewport();
 				zeroViewport();
 			case AutoZoom(minWidth, minHeight, integerScaling):
 			case AutoZoom(minWidth, minHeight, integerScaling):
 				var zoom = Math.min(engine.width / minWidth, engine.height / minHeight);
 				var zoom = Math.min(engine.width / minWidth, engine.height / minHeight);
@@ -301,17 +364,17 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 					if ( zoom == 0 ) zoom = 1;
 					if ( zoom == 0 ) zoom = 1;
 				}
 				}
 				setSceneSize(Math.ceil(engine.width / zoom), Math.ceil(engine.height / zoom));
 				setSceneSize(Math.ceil(engine.width / zoom), Math.ceil(engine.height / zoom));
-				calcRatio(zoom);
+				setViewportScale(zoom, zoom);
 				zeroViewport();
 				zeroViewport();
 		}
 		}
 	}
 	}
 
 
 	inline function screenXToViewport(mx:Float) {
 	inline function screenXToViewport(mx:Float) {
-		return mx * width / (window.width * ratioX) - viewportX;
+		return @:privateAccess interactiveCamera.screenXToCamera(window.mouseX, window.mouseY);
 	}
 	}
 
 
 	inline function screenYToViewport(my:Float) {
 	inline function screenYToViewport(my:Float) {
-		return my * height / (window.height * ratioY) - viewportY;
+		return @:privateAccess interactiveCamera.screenYToCamera(window.mouseX, window.mouseY);
 	}
 	}
 
 
 	function get_mouseX() {
 	function get_mouseX() {
@@ -388,8 +451,7 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 	}
 	}
 
 
 	function screenToViewport( e : hxd.Event ) {
 	function screenToViewport( e : hxd.Event ) {
-		e.relX = screenXToViewport(e.relX);
-		e.relY = screenYToViewport(e.relY);
+		interactiveCamera.eventToCamera(e);
 	}
 	}
 
 
 	@:dox(hide) @:noCompletion
 	@:dox(hide) @:noCompletion
@@ -641,20 +703,83 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 	}
 	}
 
 
 	override function sync( ctx : RenderContext ) {
 	override function sync( ctx : RenderContext ) {
+		var forceCamSync = posChanged;
 		if( !allocated )
 		if( !allocated )
 			onAdd();
 			onAdd();
 		super.sync(ctx);
 		super.sync(ctx);
+		for ( cam in cameras ) cam.sync(ctx, forceCamSync);
 	}
 	}
 
 
-	override function onAdd()
+	override function clipBounds(ctx:RenderContext, bounds:h2d.col.Bounds)
 	{
 	{
+		// Scene always uses whole window surface as a filter bounds as to not clip out cameras.
+		if ( rotation == 0 ) {
+			bounds.addPos(-absX, -absY);
+			bounds.addPos(window.width / matA - absX, window.height / matD - absY);
+		} else {
+			inline function calc(x:Float, y:Float) {
+				bounds.addPos(x * matA + y * matC, x * matB + y * matD);
+			}
+			var ww = window.width / matA - absX;
+			var wh = window.height / matD - absY;
+			calc(-absX, -absY);
+			calc(ww - absX, -absY);
+			calc(-absX, wh - absY);
+			calc(ww - absX, wh - absY);
+		}
+		super.clipBounds(ctx, bounds);
+	}
+
+	override function drawContent(ctx:RenderContext)
+	{
+		if( ctx.front2back ) {
+			for ( cam in cameras ) {
+				if ( !cam.visible ) continue;
+				var i = children.length;
+				var l = layerCount;
+				cam.enter(ctx);
+				while ( l-- > 0 ) {
+					var top = l == 0 ? 0 : layersIndexes[l - 1];
+					if ( cam.layerVisible(l) ) {
+						while ( i >= top ) {
+							children[i--].drawRec(ctx);
+						}
+					} else {
+						i = top - 1;
+					}
+				}
+				cam.exit(ctx);
+			}
+			draw(ctx);
+		} else {
+			draw(ctx);
+			for ( cam in cameras ) {
+				if ( !cam.visible ) continue;
+				var i = 0;
+				var l = 0;
+				cam.enter(ctx);
+				while ( l < layerCount ) {
+					var top = layersIndexes[l++];
+					if ( cam.layerVisible(l - 1) ) {
+						while ( i < top ) {
+							children[i++].drawRec(ctx);
+						}
+					} else {
+						i = top;
+					}
+				}
+				cam.exit(ctx);
+			}
+		}
+	}
+
+	override function onAdd() {
 		checkResize();
 		checkResize();
 		super.onAdd();
 		super.onAdd();
 		window.addResizeEvent(checkResize);
 		window.addResizeEvent(checkResize);
 	}
 	}
 
 
-	override function onRemove()
-	{
+	override function onRemove() {
 		super.onRemove();
 		super.onRemove();
 		window.removeResizeEvent(checkResize);
 		window.removeResizeEvent(checkResize);
 	}
 	}
@@ -673,26 +798,21 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 
 
 		var tex = target.getTexture();
 		var tex = target.getTexture();
 		engine.pushTarget(tex);
 		engine.pushTarget(tex);
-		var ow = width, oh = height, ox = offsetX, oy = offsetY;
-		var ovx = viewportX, ovy = viewportY, orx = ratioX, ory = ratioY;
+		var ow = width, oh = height, ova = viewportA, ovd = viewportD, ovx = viewportX, ovy = viewportY;
 		width = tex.width;
 		width = tex.width;
 		height = tex.height;
 		height = tex.height;
-		ratioX = 1;
-		ratioY = 1;
-		offsetX = 0;
-		offsetY = 0;
-		viewportX = 0;
-		viewportY = 0;
+		viewportA = 2 / width;
+		viewportD = 2 / height;
+		viewportX = -1;
+		viewportY = -1;
 		posChanged = true;
 		posChanged = true;
 		render(engine);
 		render(engine);
 		engine.popTarget();
 		engine.popTarget();
 
 
 		width = ow;
 		width = ow;
 		height = oh;
 		height = oh;
-		ratioX = orx;
-		ratioY = ory;
-		offsetX = ox;
-		offsetY = oy;
+		viewportA = ova;
+		viewportD = ovd;
 		viewportX = ovx;
 		viewportX = ovx;
 		viewportY = ovy;
 		viewportY = ovy;
 		posChanged = true;
 		posChanged = true;

+ 6 - 5
h3d/shader/Base2d.hx

@@ -37,7 +37,8 @@ class Base2d extends hxsl.Shader {
 		@const var killAlpha : Bool;
 		@const var killAlpha : Bool;
 		@const var pixelAlign : Bool;
 		@const var pixelAlign : Bool;
 		@param var halfPixelInverse : Vec2;
 		@param var halfPixelInverse : Vec2;
-		@param var viewport : Vec4;
+		@param var viewportA : Vec3;
+		@param var viewportB : Vec3;
 
 
 		var outputPosition : Vec4;
 		var outputPosition : Vec4;
 
 
@@ -58,13 +59,13 @@ class Base2d extends hxsl.Shader {
 		function vertex() {
 		function vertex() {
 			// transform from global to render texture coordinates
 			// transform from global to render texture coordinates
 			var tmp = vec3(absolutePosition.xy, 1);
 			var tmp = vec3(absolutePosition.xy, 1);
+			tmp = vec3(tmp.dot(filterMatrixA), tmp.dot(filterMatrixB), 1);
+			// transform to viewport
 			outputPosition = vec4(
 			outputPosition = vec4(
-				tmp.dot(filterMatrixA),
-				tmp.dot(filterMatrixB),
+				tmp.dot(viewportA),
+				tmp.dot(viewportB),
 				absolutePosition.zw
 				absolutePosition.zw
 			);
 			);
-			// transform to viewport
-			outputPosition.xy = (outputPosition.xy + viewport.xy) * viewport.zw;
 			// http://msdn.microsoft.com/en-us/library/windows/desktop/bb219690(v=vs.85).aspx
 			// http://msdn.microsoft.com/en-us/library/windows/desktop/bb219690(v=vs.85).aspx
 			if( pixelAlign ) outputPosition.xy -= halfPixelInverse;
 			if( pixelAlign ) outputPosition.xy -= halfPixelInverse;
 			output.position = outputPosition;
 			output.position = outputPosition;

+ 111 - 90
samples/Camera2D.hx

@@ -1,81 +1,53 @@
 import h3d.Engine;
 import h3d.Engine;
 import h2d.TextInput;
 import h2d.TextInput;
-import h2d.Interactive;
-import h2d.Bitmap;
-import h2d.Object;
 import h2d.Camera;
 import h2d.Camera;
-import hxd.Timer;
-import h3d.mat.Texture;
-import h2d.Tile;
-
+import h2d.Graphics;
+import h2d.TileGroup;
+import hxd.Key;
+import hxd.Res;
 
 
+//PARAM=-D resourcesPath=../../tiled_res
 class Camera2D extends SampleApp {
 class Camera2D extends SampleApp {
 
 
 	var camera : Camera;
 	var camera : Camera;
 
 
-	var bg : Bitmap;
-	var inter : Interactive;
+	var followCamera : Camera;
+	var followPoint : Graphics;
 
 
 	var sliderAnchorX : h2d.Slider;
 	var sliderAnchorX : h2d.Slider;
 	var sliderCamAnchorX : h2d.Slider;
 	var sliderCamAnchorX : h2d.Slider;
 	var sliderCamAnchorY : h2d.Slider;
 	var sliderCamAnchorY : h2d.Slider;
-	var sliderCamViewX : h2d.Slider;
-	var sliderCamViewY : h2d.Slider;
 	var sliderCamX : h2d.Slider;
 	var sliderCamX : h2d.Slider;
 	var sliderCamY : h2d.Slider;
 	var sliderCamY : h2d.Slider;
 	var sliderCamScaleX : h2d.Slider;
 	var sliderCamScaleX : h2d.Slider;
-	var sliderCamScaleY : h2d.Slider;	
+	var sliderCamScaleY : h2d.Slider;
 
 
-	var anchorMarkerScreenSpace : h2d.Graphics;
-	var anchorMarkerSceneSpaceA : h2d.Bitmap;
-	var anchorMarkerSceneSpaceB : h2d.Bitmap;
+	var cameraPositionMarker : h2d.Graphics;
 
 
 	var reportCameraParameterChangedAfterSync : Bool = false;
 	var reportCameraParameterChangedAfterSync : Bool = false;
 
 
-	static inline var scaleContentRelativeToScene : Bool = false;
-
-	override function onResize()  {
+	override function onResize() {
 		super.onResize();
 		super.onResize();
 
 
-		//change slide min max values according to screen size
-		sliderCamViewX.minValue = -s2d.width*2; sliderCamViewX.maxValue = s2d.width*2;
-		sliderCamViewY.minValue = -s2d.height*2; sliderCamViewY.maxValue = s2d.height*2;
-		
-		sliderCamX.minValue = -s2d.width; sliderCamX.maxValue = s2d.width; 
-		sliderCamY.minValue = -s2d.height; sliderCamY.maxValue = s2d.height; 
-
-		//scale content relative to scene
-		if(scaleContentRelativeToScene)
-		{
-			bg.tile.scaleToSize(s2d.width,s2d.height);
-
-			inter.x = s2d.width * .5 - 50;
-			inter.y = s2d.height * .5 - 80;
-		}
+		// Change slider min max values according to screen size
+		sliderCamX.minValue = -s2d.width; sliderCamX.maxValue = s2d.width;
+		sliderCamY.minValue = -s2d.height; sliderCamY.maxValue = s2d.height;
 
 
 		onCameraParameterChanged();
 		onCameraParameterChanged();
 		reportCameraParameterChangedAfterSync=true;
 		reportCameraParameterChangedAfterSync=true;
 	}
 	}
 
 
-	private function setSliderAndTextInputValue(slider : h2d.Slider, value : Float)
-	{
-		slider.value = value; 
-		var  tf : h2d.TextInput = hxd.impl.Api.downcast(slider.parent.getChildAt(2),h2d.TextInput);		
-		if(tf!=null) tf.text = "" + hxd.Math.fmt(value);	
+	private function setSliderAndTextInputValue( slider : h2d.Slider, value : Float ) {
+		slider.value = value;
+		var tf : h2d.TextInput = hxd.impl.Api.downcast(slider.parent.getChildAt(2),h2d.TextInput);
+		if(tf!=null) tf.text = "" + hxd.Math.fmt(value);
 	}
 	}
 
 
-	private function onCameraParameterChanged()
-	{
+	private function onCameraParameterChanged() {
 		updateCamSliderValues();
 		updateCamSliderValues();
 
 
-		anchorMarkerScreenSpace.x=camera.anchorX*s2d.width;
-		anchorMarkerScreenSpace.y=camera.anchorY*s2d.height;
-
-		anchorMarkerSceneSpaceA.x=camera.viewX/camera.scaleX-20;
-		anchorMarkerSceneSpaceA.y=camera.viewY/camera.scaleY-2;
-
-		anchorMarkerSceneSpaceB.x=camera.viewX/camera.scaleX-2;
-		anchorMarkerSceneSpaceB.y=camera.viewY/camera.scaleY-20;
+		cameraPositionMarker.x = camera.x;
+		cameraPositionMarker.y = camera.y;
 	}
 	}
 
 
 	private function updateCamSliderValues() {
 	private function updateCamSliderValues() {
@@ -83,9 +55,6 @@ class Camera2D extends SampleApp {
 		setSliderAndTextInputValue(sliderCamAnchorX, camera.anchorX);
 		setSliderAndTextInputValue(sliderCamAnchorX, camera.anchorX);
 		setSliderAndTextInputValue(sliderCamAnchorY, camera.anchorY);
 		setSliderAndTextInputValue(sliderCamAnchorY, camera.anchorY);
 
 
-		setSliderAndTextInputValue(sliderCamViewX, camera.viewX);
-		setSliderAndTextInputValue(sliderCamViewY, camera.viewY);
-
 		setSliderAndTextInputValue(sliderCamScaleX, camera.scaleX);
 		setSliderAndTextInputValue(sliderCamScaleX, camera.scaleX);
 		setSliderAndTextInputValue(sliderCamScaleY, camera.scaleY);
 		setSliderAndTextInputValue(sliderCamScaleY, camera.scaleY);
 
 
@@ -94,51 +63,87 @@ class Camera2D extends SampleApp {
 
 
 	}
 	}
 
 
-	override private function init()
-	{
+	override private function init() {
 		super.init();
 		super.init();
+		
+		// Second camera for sample controls
+		var uiCamera = new Camera();
+		// layerVisible allows to filter out layers that camera should not render.
+		uiCamera.layerVisible = (idx) -> idx == 2;
+		s2d.add(fui, 2);
+		// Add UI camera to scene. Note that order of cameras in array matters, as they are rendered in-order.
+		s2d.addCamera(uiCamera);
+		// Only one camera can handle user input events.
+		// When assigning newly-created camera as interactiveCamera - adding it to Scene can be omitted, as it will be added automatically.
+		s2d.interactiveCamera = uiCamera;
+
+		// See Tiled sample
+		var followX = s2d.width * .5;
+		var followY = s2d.height * .5;
+		var tileSize = 16;
+		var tmx = Res.tileMap.toMap();
+
+		var tset = Res.tiles.toTile();
+		var tiles = tset.gridFlatten(tileSize, 0, 0);
+		for ( l in tmx.layers ) {
+			var group : TileGroup = new TileGroup(tset);
+			// Props layer won't be visible on main camera, but will be visible in the follower camera.
+			s2d.add(group, l.name == "Props" ? 1 : 0);
+			group.x = followX - tmx.width * (tileSize / 2);
+			group.y = followY - tmx.height * (tileSize / 2);
+			var y = 0, x = 0;
+			for (gid in l.data) {
+				if (gid != 0) group.add(x * tileSize, y * tileSize, tiles[gid-1]);
+				if (++x == tmx.width) {
+					x = 0;
+					y++;
+				}
+			}
+		}
 
 
-		// Initialize camera.
-		camera = new Camera(s2d);
-
-		// Backdrop to show the camera frame.
-		bg = new h2d.Bitmap(h2d.Tile.fromColor(0xffffff, s2d.width, s2d.height, 0.2), camera);
-
-		// Interactive inside camera
-		inter = new h2d.Interactive(100, 40, camera);
-		inter.backgroundColor = 0xff0000ff;
-		inter.x = s2d.width * .5 - 50;
-		inter.y = s2d.height * .5 - 80;
-		var interText = new h2d.Text(getFont(), inter);
-		interText.textAlign = Center;
-		interText.maxWidth = 100;
-		interText.text = "In-camera Interactive";
-
-		//anchor marker in screen space
-		anchorMarkerScreenSpace = new h2d.Graphics(s2d);
-		anchorMarkerScreenSpace.x=camera.anchorX*s2d.width;
-		anchorMarkerScreenSpace.y=camera.anchorY*s2d.height;
-		anchorMarkerScreenSpace.beginFill(0xff0000,0.5);
-		anchorMarkerScreenSpace.drawRect(-10, -1, 20, 2);
-		anchorMarkerScreenSpace.drawRect(-1, -10, 2, 20);
-		anchorMarkerScreenSpace.endFill();
-
-		//anchor marker in screen space
-		anchorMarkerSceneSpaceA = new h2d.Bitmap(h2d.Tile.fromColor(0xfffffff, 40,4, 0.5), camera);
-		anchorMarkerSceneSpaceA.x=camera.viewX/camera.scaleX-20;
-		anchorMarkerSceneSpaceA.y=camera.viewY/camera.scaleY-2;
-		anchorMarkerSceneSpaceB = new h2d.Bitmap(h2d.Tile.fromColor(0xfffffff, 4,40, 0.5), camera);
-		anchorMarkerSceneSpaceB.x=camera.viewX/camera.scaleX-2;
-		anchorMarkerSceneSpaceB.y=camera.viewY/camera.scaleY-20;
-
-		addText("Camera");
+		addText("User arrow keys to move the green arrow");
+		followPoint = new Graphics(s2d);
+		followPoint.beginFill(0xff00);
+		followPoint.moveTo(0, -5);
+		followPoint.lineTo(5, 5);
+		followPoint.lineTo(-5, 5);
+		followPoint.setPosition(followX, followY);
+
+		// Anchor allows to adjust the position of camera target relative to it's top-left corner in scene viewport ratio.
+		// 0.5 would ensure that whatever position camera points at would at the center of it's viewport.
+		// Providing Scene instance to camera constructor automatically adds it to the Scene camera list.
+		followCamera = new Camera(s2d);
+		// Set viewport to take up bottom-left quarter of the screen and clip out contents outside of it.
+		followCamera.setAnchor(0.5, 0.5);
+		followCamera.setViewport(s2d.width * .5, s2d.height * .5, s2d.width * .5, s2d.height * .5);
+		followCamera.setScale(2, 2);
+		followCamera.clipViewport = true;
+		followCamera.layerVisible = (idx) -> idx != 2; // skip UI layer
+		followCamera.follow = followPoint;
+		followCamera.followRotation = true;
+
+		// Scene.camera proeprty provides an alias to `Scene.cameras[0]`.
+		camera = s2d.camera;
+		camera.setAnchor(0.5, 0.5);
+		camera.setPosition(s2d.width * .5, s2d.height * .5);
+		camera.layerVisible = (idx) -> idx == 0;
+
+		// Marker for primary camera position
+		cameraPositionMarker = new h2d.Graphics(s2d);
+		cameraPositionMarker.x= camera.x;
+		cameraPositionMarker.y= camera.y;
+		cameraPositionMarker.beginFill(0xff0000,0.5);
+		cameraPositionMarker.drawRect(-10, -1, 20, 2);
+		cameraPositionMarker.drawRect(-1, -10, 2, 20);
+		cameraPositionMarker.endFill();
+
+		addText("Camera controls");
 		sliderCamAnchorX=addSlider("Anchor X", function() { return camera.anchorX; }, function(v) { camera.anchorX = v; onCameraParameterChanged();}, 0, 1);
 		sliderCamAnchorX=addSlider("Anchor X", function() { return camera.anchorX; }, function(v) { camera.anchorX = v; onCameraParameterChanged();}, 0, 1);
 		sliderCamAnchorY=addSlider("Anchor Y", function() { return camera.anchorY; }, function(v) { camera.anchorY = v; onCameraParameterChanged();}, 0, 1);
 		sliderCamAnchorY=addSlider("Anchor Y", function() { return camera.anchorY; }, function(v) { camera.anchorY = v; onCameraParameterChanged();}, 0, 1);
-		sliderCamViewX=addSlider("View X", function() { return camera.viewX; }, function(v) { camera.viewX = v; onCameraParameterChanged();}, -s2d.width*2, s2d.width*2);
-		sliderCamViewY=addSlider("View Y", function() { return camera.viewY; }, function(v) { camera.viewY = v; onCameraParameterChanged();}, -s2d.height*2, s2d.height*2);
 		sliderCamX=addSlider("X", function() { return camera.x; }, function(v) { camera.x = v; onCameraParameterChanged();}, -s2d.width, s2d.width);
 		sliderCamX=addSlider("X", function() { return camera.x; }, function(v) { camera.x = v; onCameraParameterChanged();}, -s2d.width, s2d.width);
 		sliderCamY=addSlider("Y", function() { return camera.y; }, function(v) { camera.y = v; onCameraParameterChanged();}, -s2d.height, s2d.height);
 		sliderCamY=addSlider("Y", function() { return camera.y; }, function(v) { camera.y = v; onCameraParameterChanged();}, -s2d.height, s2d.height);
-		// addSlider("Rotation", function() { return hxd.Math.radToDeg(camera.rotation); }, function(v) { camera.rotation = hxd.Math.degToRad(v); onCameraParameterChanged(); }, 0, 360);
+		// Scale and rotation happens around anchored position, so in case of anchor [0.5, 0.5] it would scale and rotate around center of the camera viewport.
+		addSlider("Rotation", function() { return hxd.Math.radToDeg(camera.rotation); }, function(v) { camera.rotation = hxd.Math.degToRad(v); onCameraParameterChanged(); }, 0, 360);
 		sliderCamScaleX=addSlider("Scale X", function() { return camera.scaleX; }, function(v) { camera.scaleX = v; onCameraParameterChanged();}, 0, 5);
 		sliderCamScaleX=addSlider("Scale X", function() { return camera.scaleX; }, function(v) { camera.scaleX = v; onCameraParameterChanged();}, 0, 5);
 		sliderCamScaleY=addSlider("Scale Y", function() { return camera.scaleY; }, function(v) { camera.scaleY = v; onCameraParameterChanged();}, 0, 5);
 		sliderCamScaleY=addSlider("Scale Y", function() { return camera.scaleY; }, function(v) { camera.scaleY = v; onCameraParameterChanged();}, 0, 5);
 
 
@@ -155,6 +160,22 @@ class Camera2D extends SampleApp {
 
 
 	override function update(dt:Float) {
 	override function update(dt:Float) {
 		super.update(dt);
 		super.update(dt);
+		if (Key.isDown(Key.SHIFT)) dt *= 3;
+		if (Key.isDown(Key.LEFT)) followPoint.rotation -= dt;
+		if (Key.isDown(Key.RIGHT)) followPoint.rotation += dt;
+		var forward = followPoint.rotation - Math.PI * .5;
+		if (Key.isDown(Key.UP)) {
+			followPoint.x += Math.cos(forward) * 60 * dt;
+			followPoint.y += Math.sin(forward) * 60 * dt;
+		}
+		if (Key.isDown(Key.DOWN)) {
+			followPoint.x -= Math.cos(forward) * 60 * dt;
+			followPoint.y -= Math.sin(forward) * 60 * dt;
+		}
+		if (Key.isReleased(Key.SPACE)) {
+			followPoint.setPosition(s2d.width * .5, s2d.height * .5);
+			followPoint.rotation = 0;
+		}
 	}
 	}
 
 
 	static function main() {
 	static function main() {