Pavel Alexandrov 5 лет назад
Родитель
Сommit
9fb2ff1daa
7 измененных файлов с 883 добавлено и 372 удалено
  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;
 
-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;
-	/** 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;
 
-	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;
 
-	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 ) {
 			var f1 = ctx.baseShader.filterMatrixA;
 			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 {
-			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
@@ -670,10 +676,10 @@ class Object #if (domkit && !domkit_heaps) implements domkit.Model<h2d.Object> #
 
 		// clip with our scene
 		@: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
@@ -742,7 +748,6 @@ class Object #if (domkit && !domkit_heaps) implements domkit.Model<h2d.Object> #
 		var shader = @:privateAccess ctx.baseShader;
 		var oldA = shader.filterMatrixA.clone();
 		var oldB = shader.filterMatrixB.clone();
-		var oldF = @:privateAccess ctx.inFilter;
 
 		// 2x3 inverse matrix
 		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.filterMatrixB.set(invB, invD, invY);
 		ctx.globalAlpha = 1;
-		draw(ctx);
-		for( c in children )
-			c.drawRec(ctx);
+		drawContent(ctx);
 		ctx.flush();
 
 		var finalTile = h2d.Tile.fromTexture(t);
@@ -815,18 +818,22 @@ class Object #if (domkit && !domkit_heaps) implements domkit.Model<h2d.Object> #
 		} else {
 			var old = ctx.globalAlpha;
 			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;
 		}
 	}
 
+	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) {
 		posChanged = true;
 		return x = v;

+ 153 - 91
h2d/RenderContext.hx

@@ -1,7 +1,15 @@
 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 };
 
+@:access(h2d.Scene)
 class RenderContext extends h3d.impl.RenderContext {
 
 	static inline var BUFFERING = false;
@@ -30,8 +38,11 @@ class RenderContext extends h3d.impl.RenderContext {
 	var baseShaderList : hxsl.ShaderList;
 	var currentObj : Drawable;
 	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 cameraStack : Array<CameraStackEntry>;
+	var cameraStackIndex : Int;
+	var curTarget : h3d.mat.Texture;
 	var renderZoneStack:Array<RenderZoneStack> = [];
 	var renderZoneIndex:Int = 0;
 	var hasUVPos : Bool;
@@ -39,10 +50,12 @@ class RenderContext extends h3d.impl.RenderContext {
 	var inFilter : Object;
 	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 renderX : Float;
@@ -69,6 +82,8 @@ class RenderContext extends h3d.impl.RenderContext {
 		baseShaderList = new hxsl.ShaderList(baseShader);
 		targetsStack = [];
 		targetsStackIndex = 0;
+		cameraStack = [];
+		cameraStackIndex = 0;
 		filterStack = [];
 	}
 
@@ -86,19 +101,23 @@ class RenderContext extends h3d.impl.RenderContext {
 		currentObj = null;
 		bufPos = 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;
 		baseFlipY = engine.getCurrentTarget() != null ? targetFlipY : 1;
 		inFilter = null;
-		curWidth = scene.width;
-		curHeight = scene.height;
 		manager.globals.set("time", time);
 		manager.globals.set("global.time", time);
 		// 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.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.filterMatrixB.set(0, 1, 0);
 		baseShaderList.next = null;
@@ -134,7 +153,52 @@ class RenderContext extends h3d.impl.RenderContext {
 		texture = null;
 		currentObj = 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 ) {
@@ -159,30 +223,39 @@ class RenderContext extends h3d.impl.RenderContext {
 		flush();
 		engine.pushTarget(t);
 		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( 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.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;
 		if( hasRenderZone ) clearRZ();
 	}
@@ -195,48 +268,27 @@ class RenderContext extends h3d.impl.RenderContext {
 		}
 	}
 
-	public function popTarget( restore = true ) {
+	public function popTarget() {
 		flush();
 		if( targetsStackIndex <= 0 ) throw "Too many popTarget()";
-		var pinf = targetsStack[--targetsStackIndex];
 		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 ) {
@@ -293,8 +345,8 @@ class RenderContext extends h3d.impl.RenderContext {
 		renderY = y;
 		renderW = w;
 		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 ) {
 			var fa = baseShader.filterMatrixA;
 			var fb = baseShader.filterMatrixB;
@@ -308,7 +360,13 @@ class RenderContext extends h3d.impl.RenderContext {
 			w = rx2 - rx1;
 			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() {
@@ -412,29 +470,35 @@ class RenderContext extends h3d.impl.RenderContext {
 		if( inFilter != null ) {
 			var f1 = baseShader.filterMatrixA;
 			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 {
-			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
 		if( matB == 0 && matC == 0 ) {
 			var tx = tile.dx + tile.width * 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 {
 			var xMin = 1e20, yMin = 1e20, xMax = -1e20, yMax = -1e20;
 			inline function calc(x:Float, y:Float) {
@@ -451,9 +515,7 @@ class RenderContext extends h3d.impl.RenderContext {
 			calc(tile.width, 0);
 			calc(0, 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;
 		}
 

+ 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;
 
+	/**
+		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.
-		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.
-		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.
@@ -137,6 +148,26 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 	**/
 	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)
 	**/
@@ -162,14 +193,19 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 		super(null);
 		var e = h3d.Engine.getCurrent();
 		ctx = new RenderContext(this);
+		_cameras = [];
+		new Camera(this);
+		interactiveCamera = camera;
 		width = e.width;
 		height = e.height;
+		viewportA = 2 / e.width;
+		viewportD = 2 / e.height;
+		viewportX = -1;
+		viewportY = -1;
+		viewportScaleX = 1;
+		viewportScaleY = 1;
 		offsetX = 0;
 		offsetY = 0;
-		ratioX = 1;
-		ratioY = 1;
-		viewportX = 0;
-		viewportY = 0;
 		interactive = new Array();
 		eventListeners = new Array();
 		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 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.
 	**/
@@ -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 ) {
+			viewportA = (zoom * 2) / engine.width;
+			viewportD = (zoom * 2) / engine.height;
+			setViewportScale(zoom, zoom);
 			if ( horizontal == null ) horizontal = Center;
 			switch ( horizontal ) {
 				case Left:
-					offsetX = (engine.width - width * zoom) / (2 * zoom);
-					viewportX = 0;
+					viewportX = -1;
+					offsetX = 0;
 				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:
-					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;
 			switch ( vertical ) {
 				case Top:
-					offsetY = (engine.height - height * zoom) / (2 * zoom);
-					viewportY = 0;
+					viewportY = -1;
+					offsetY = 0;
 				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:
-					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() {
-			offsetX = 0;
-			offsetY = 0;
-			viewportX = 0;
-			viewportY = 0;
+			viewportA = 2 / width;
+			viewportD = 2 / height;
+			viewportX = -1;
+			viewportY = -1;
 		}
 
 		switch ( scaleMode ) {
 			case Resize:
 				setSceneSize(engine.width, engine.height);
-				ratioX = 1;
-				ratioY = 1;
+				setViewportScale(1, 1);
 				zeroViewport();
 			case Stretch(_width, _height):
 				setSceneSize(_width, _height);
-				ratioX = 1;
-				ratioY = 1;
+				setViewportScale(engine.width / _width, engine.height / _height);
 				zeroViewport();
 			case LetterBox(_width, _height, integerScale, horizontalAlign, verticalAlign):
 				setSceneSize(_width, _height);
@@ -284,15 +349,13 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 					zoom = Std.int(zoom);
 					if (zoom == 0) zoom = 1;
 				}
-				calcRatio(zoom);
 				calcViewport(horizontalAlign, verticalAlign, zoom);
 			case Fixed(_width, _height, zoom, horizontalAlign, verticalAlign):
 				setSceneSize(_width, _height);
-				calcRatio(zoom);
 				calcViewport(horizontalAlign, verticalAlign, zoom);
 			case Zoom(level):
 				setSceneSize(Math.ceil(engine.width / level), Math.ceil(engine.height / level));
-				calcRatio(level);
+				setViewportScale(level, level);
 				zeroViewport();
 			case AutoZoom(minWidth, minHeight, integerScaling):
 				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;
 				}
 				setSceneSize(Math.ceil(engine.width / zoom), Math.ceil(engine.height / zoom));
-				calcRatio(zoom);
+				setViewportScale(zoom, zoom);
 				zeroViewport();
 		}
 	}
 
 	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) {
-		return my * height / (window.height * ratioY) - viewportY;
+		return @:privateAccess interactiveCamera.screenYToCamera(window.mouseX, window.mouseY);
 	}
 
 	function get_mouseX() {
@@ -388,8 +451,7 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 	}
 
 	function screenToViewport( e : hxd.Event ) {
-		e.relX = screenXToViewport(e.relX);
-		e.relY = screenYToViewport(e.relY);
+		interactiveCamera.eventToCamera(e);
 	}
 
 	@:dox(hide) @:noCompletion
@@ -641,20 +703,83 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 	}
 
 	override function sync( ctx : RenderContext ) {
+		var forceCamSync = posChanged;
 		if( !allocated )
 			onAdd();
 		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();
 		super.onAdd();
 		window.addResizeEvent(checkResize);
 	}
 
-	override function onRemove()
-	{
+	override function onRemove() {
 		super.onRemove();
 		window.removeResizeEvent(checkResize);
 	}
@@ -673,26 +798,21 @@ class Scene extends Layers implements h3d.IDrawable implements hxd.SceneEvents.I
 
 		var tex = target.getTexture();
 		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;
 		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;
 		render(engine);
 		engine.popTarget();
 
 		width = ow;
 		height = oh;
-		ratioX = orx;
-		ratioY = ory;
-		offsetX = ox;
-		offsetY = oy;
+		viewportA = ova;
+		viewportD = ovd;
 		viewportX = ovx;
 		viewportY = ovy;
 		posChanged = true;

+ 6 - 5
h3d/shader/Base2d.hx

@@ -37,7 +37,8 @@ class Base2d extends hxsl.Shader {
 		@const var killAlpha : Bool;
 		@const var pixelAlign : Bool;
 		@param var halfPixelInverse : Vec2;
-		@param var viewport : Vec4;
+		@param var viewportA : Vec3;
+		@param var viewportB : Vec3;
 
 		var outputPosition : Vec4;
 
@@ -58,13 +59,13 @@ class Base2d extends hxsl.Shader {
 		function vertex() {
 			// transform from global to render texture coordinates
 			var tmp = vec3(absolutePosition.xy, 1);
+			tmp = vec3(tmp.dot(filterMatrixA), tmp.dot(filterMatrixB), 1);
+			// transform to viewport
 			outputPosition = vec4(
-				tmp.dot(filterMatrixA),
-				tmp.dot(filterMatrixB),
+				tmp.dot(viewportA),
+				tmp.dot(viewportB),
 				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
 			if( pixelAlign ) outputPosition.xy -= halfPixelInverse;
 			output.position = outputPosition;

+ 111 - 90
samples/Camera2D.hx

@@ -1,81 +1,53 @@
 import h3d.Engine;
 import h2d.TextInput;
-import h2d.Interactive;
-import h2d.Bitmap;
-import h2d.Object;
 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 {
 
 	var camera : Camera;
 
-	var bg : Bitmap;
-	var inter : Interactive;
+	var followCamera : Camera;
+	var followPoint : Graphics;
 
 	var sliderAnchorX : h2d.Slider;
 	var sliderCamAnchorX : h2d.Slider;
 	var sliderCamAnchorY : h2d.Slider;
-	var sliderCamViewX : h2d.Slider;
-	var sliderCamViewY : h2d.Slider;
 	var sliderCamX : h2d.Slider;
 	var sliderCamY : 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;
 
-	static inline var scaleContentRelativeToScene : Bool = false;
-
-	override function onResize()  {
+	override function 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();
 		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();
 
-		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() {
@@ -83,9 +55,6 @@ class Camera2D extends SampleApp {
 		setSliderAndTextInputValue(sliderCamAnchorX, camera.anchorX);
 		setSliderAndTextInputValue(sliderCamAnchorY, camera.anchorY);
 
-		setSliderAndTextInputValue(sliderCamViewX, camera.viewX);
-		setSliderAndTextInputValue(sliderCamViewY, camera.viewY);
-
 		setSliderAndTextInputValue(sliderCamScaleX, camera.scaleX);
 		setSliderAndTextInputValue(sliderCamScaleY, camera.scaleY);
 
@@ -94,51 +63,87 @@ class Camera2D extends SampleApp {
 
 	}
 
-	override private function init()
-	{
+	override private function 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);
 		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);
 		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);
 		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) {
 		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() {