Browse Source

Adding cascade shadow mapping.

clandrin 2 years ago
parent
commit
08a214d8fc
4 changed files with 470 additions and 36 deletions
  1. 366 0
      h3d/pass/CascadeShadowMap.hx
  2. 38 34
      h3d/pass/DirShadowMap.hx
  3. 2 2
      h3d/scene/pbr/DirLight.hx
  4. 64 0
      h3d/shader/CascadeShadow.hx

+ 366 - 0
h3d/pass/CascadeShadowMap.hx

@@ -0,0 +1,366 @@
+package h3d.pass;
+
+class CascadeShadowMap extends DirShadowMap {
+
+	var cshader : h3d.shader.CascadeShadow;
+	var lightCameras : Array<h3d.Camera> = [];
+	var currentCascadeIndex = 0;
+
+	public var pow : Float = 1.0;
+	public var firstCascadeSize : Float = 10.0;
+	public var cascade(default, set) = 1;
+	public var debug : Bool = false;
+	public function set_cascade(v) {
+		cascade = v;
+		lightCameras = [];
+		for ( i in 0...cascade ) {
+			lightCameras.push(new h3d.Camera());
+			lightCameras[i].orthoBounds = new h3d.col.Bounds();
+		}
+		return cascade;
+	}
+
+	public function new( light : h3d.scene.Light ) {
+		super(light);
+		shader = dshader = cshader = new h3d.shader.CascadeShadow();
+	}
+
+	public override function getShadowTex() {
+		return cshader.shadowMap;
+	}
+
+	public function getShadowTextures() {
+		return cshader.cascadeShadowMaps;
+	}
+
+	public dynamic function calcBounds( camera : h3d.Camera, ?limits : h3d.col.Bounds) {
+		var bounds = camera.orthoBounds;
+		var zMax = -1e9, zMin = 1e9;
+		// add visible casters in light camera position
+		var mtmp = new h3d.Matrix();
+		var btmp = autoZPlanes ? new h3d.col.Bounds() : null;
+		var obj = boundingObject != null ? boundingObject : ctx.scene;
+		obj.iterVisibleMeshes(function(m) {
+			if( m.primitive == null || !m.material.castShadows ) return;
+			var b = m.primitive.getBounds();
+			if( b.xMin > b.xMax ) return;
+
+			var absPos = Std.isOfType(m.primitive, h3d.prim.Instanced) ? h3d.Matrix.I() : m.getAbsPos();
+			if( autoZPlanes ) {
+				btmp.load(b);
+				btmp.transform(absPos);
+				if( btmp.zMax > zMax ) zMax = btmp.zMax;
+				if( btmp.zMin < zMin ) zMin = btmp.zMin;
+			}
+
+			var points = [];
+			mtmp.multiply3x4(absPos, camera.mcam);
+
+			var p = new h3d.col.Point(b.xMin, b.yMin, b.zMin);
+			p.transform(mtmp);
+			points.push(p);
+
+			var p = new h3d.col.Point(b.xMin, b.yMin, b.zMax);
+			p.transform(mtmp);
+			points.push(p);
+
+			var p = new h3d.col.Point(b.xMin, b.yMax, b.zMin);
+			p.transform(mtmp);
+			points.push(p);
+
+			var p = new h3d.col.Point(b.xMin, b.yMax, b.zMax);
+			p.transform(mtmp);
+			points.push(p);				
+
+			var p = new h3d.col.Point(b.xMax, b.yMin, b.zMin);
+			p.transform(mtmp);
+			points.push(p);
+
+			var p = new h3d.col.Point(b.xMax, b.yMin, b.zMax);
+			p.transform(mtmp);
+			points.push(p);
+
+			var p = new h3d.col.Point(b.xMax, b.yMax, b.zMin);
+			p.transform(mtmp);
+			points.push(p);
+
+			var p = new h3d.col.Point(b.xMax, b.yMax, b.zMax);
+			p.transform(mtmp);
+			points.push(p);
+
+			var add = true;
+			if ( limits != null ) {
+				add = false;
+				for ( p in points ) {
+					if ( limits.contains(p) ) {
+						add = true;
+						break;
+					}
+				}
+			}
+			if ( add ) {
+				for ( p in points ) {
+					bounds.addPoint(p);
+				}
+			}
+
+		});
+
+		if( mode == Dynamic ) {
+
+			// Intersect with frustum bounds
+			var cameraBounds = new h3d.col.Bounds();
+			var minDist = minDist < 0 ? ctx.camera.zNear : minDist;
+			var maxDist = maxDist < 0 ? ctx.camera.zFar : maxDist;
+
+			inline function addCorner(x,y,d) {
+				var dist = d?minDist:maxDist;
+				var pt = ctx.camera.unproject(x,y,ctx.camera.distanceToDepth(dist)).toPoint();
+				if( autoZPlanes ) {
+					// let's auto limit our frustrum to our zMin/Max planes
+					var r = h3d.col.Ray.fromPoints(ctx.camera.pos.toPoint(), pt);
+					var d2 = r.distance(h3d.col.Plane.Z(d?zMax:zMin));
+					var k = d ? 1 : -1;
+					if( d2 > 0 && d2*k > dist*k )
+						pt.load(r.getPoint(d2));
+				}
+				pt.transform(camera.mcam);
+				cameraBounds.addPos(pt.x, pt.y, pt.z);
+			}
+
+			inline function addCorners(d) {
+				addCorner(-1,-1,d);
+				addCorner(-1,1,d);
+				addCorner(1,-1,d);
+				addCorner(1,1,d);
+			}
+
+			addCorners(true);
+			addCorners(false);
+
+			// Keep the zMin from the bounds of visible objects
+			// Prevent shadows inside frustum from objects outside frustum being clipped
+			cameraBounds.zMin = bounds.zMin;
+			bounds.intersection(bounds, cameraBounds);
+			if( autoZPlanes ) {
+				/*
+					Let's intersect again our light camera bounds with our scene zMax plane
+					so we can tighten the zMin, which will give us better precision
+					for our depth map.
+				*/
+				var v = camera.target.sub(camera.pos).normalized();
+				var dMin = 1e9;
+				for( dx in 0...2 )
+					for( dy in 0...2 ) {
+						var px = dx > 0 ? bounds.xMax : bounds.xMin;
+						var py = dy > 0 ? bounds.yMax : bounds.yMin;
+						var r0 = new h3d.col.Point(px,py,bounds.zMin).transformed(camera.getInverseView());
+						var r = h3d.col.Ray.fromValues(r0.x, r0.y, r0.z, v.x, v.y, v.z);
+						var d = r.distance(h3d.col.Plane.Z(zMax));
+						if( d < dMin ) dMin = d;
+					}
+				bounds.zMin += dMin;
+			}
+		}
+		bounds.scaleCenter(1.01);
+	}
+
+	public function calcCascadeShadowBounds( camera : h3d.Camera ) {
+		calcBounds(camera);
+		var bounds = camera.orthoBounds;
+
+		lightCamera.update();
+		var shadowNear = hxd.Math.POSITIVE_INFINITY;
+		var shadowFar = hxd.Math.NEGATIVE_INFINITY;
+		var corners = lightCamera.getFrustumCorners();
+		for ( corner in corners ) {
+			corner.transform(ctx.camera.mcam);
+			shadowNear = hxd.Math.min(shadowNear, corner.z / corner.w);
+			shadowFar = hxd.Math.max(shadowFar, corner.z / corner.w);
+		}
+		var near = shadowNear;
+		var far = shadowNear + firstCascadeSize;
+		for ( i in 0...cascade ) {
+			var cascadeBounds = new h3d.col.Bounds();
+			function addCorner(x,y,d) {
+				var pt = ctx.camera.unproject(x,y,ctx.camera.distanceToDepth(d)).toPoint();
+				pt.transform(camera.mcam);
+				cascadeBounds.addPos(pt.x, pt.y, pt.z);
+			}
+			function addCorners(d) {
+				addCorner(-1,-1,d);
+				addCorner(-1,1,d);
+				addCorner(1,-1,d);
+				addCorner(1,1,d);
+			}
+
+			if ( i == cascade - 1 )
+				far = shadowFar;
+			addCorners(near);
+			addCorners(hxd.Math.min(far, shadowFar));
+			lightCameras[i].orthoBounds = bounds.clone();
+			lightCameras[i].orthoBounds.intersection(lightCameras[i].orthoBounds, cascadeBounds);
+			var limits = lightCameras[i].orthoBounds.clone();
+			lightCameras[i].orthoBounds.empty();
+			calcBounds(lightCameras[i], limits);
+			lightCameras[i].orthoBounds.intersection(lightCameras[i].orthoBounds, limits);
+
+			near += firstCascadeSize * hxd.Math.pow(pow, i);
+			far += firstCascadeSize * hxd.Math.pow(pow, i+1);
+		}
+	}
+
+	override function setGlobals() {
+		super.setGlobals();
+		cameraViewProj = getCascadeProj(currentCascadeIndex);
+	}
+
+	function getCascadeProj(i:Int) {
+		return lightCameras[i].m;
+	}
+
+	function syncCascadeShader(textures : Array<h3d.mat.Texture>) {
+		for ( i in 0...cascade ) {
+			cshader.cascadeShadowMaps[i] = textures[i];
+			cshader.cascadeProjs[i] = lightCameras[i].m;
+		}
+		for ( i in 0...cascade-1 ) {
+			var pt = lightCameras[i].unproject(0,0,1);
+			pt.transform(ctx.camera.m);
+			cshader.cascadeLimits[i] = pt.z;
+		}
+		var pt = lightCamera.unproject(0,0,1);
+		pt.transform(ctx.camera.m);
+		cshader.cascadeLimits[cascade-1] = pt.z;
+		cshader.camViewProj = ctx.camera.m;
+		cshader.CASCADE_COUNT = cascade;
+		cshader.shadowBias = bias * 2.0; // experimental value for consistency with/without cascades
+		cshader.shadowPower = power;
+		cshader.shadowProj = getShadowProj();
+
+		//ESM
+		cshader.USE_ESM = samplingKind == ESM;
+		cshader.shadowPower = power;
+
+		// PCF
+		cshader.USE_PCF = samplingKind == PCF;
+		cshader.shadowRes.set(textures[0].width,textures[0].height);
+		cshader.pcfScale = pcfScale * 5.0; // experimental value for consistency with/without cascades
+		cshader.pcfQuality = pcfQuality;
+	}
+
+	override function draw( passes, ?sort ) {
+		if ( g != null )
+			g.clear();
+
+		if( !enabled )
+			return;
+
+		if( !filterPasses(passes) )
+			return;
+
+		if( mode != Mixed || ctx.computingStatic ) {
+			lightCamera.orthoBounds.empty();
+			for ( lC in lightCameras ) lC.orthoBounds.empty();
+			if( !passes.isEmpty() ) calcCascadeShadowBounds(lightCamera);
+			lightCamera.update();
+			for ( lC in lightCameras ) lC.update();
+		}
+
+		cullPasses(passes,function(col) return col.inFrustum(lightCamera.frustum));
+
+		if( mode != Mixed || ctx.computingStatic ) {
+			var ct = ctx.camera.target;
+			var slight = light == null ? ctx.lightSystem.shadowLight : light;
+			var ldir = slight == null ? null : @:privateAccess slight.getShadowDirection();
+			if( ldir == null )
+				lightCamera.target.set(0, 0, -1);
+			else {
+				lightCamera.target.set(ldir.x, ldir.y, ldir.z);
+				lightCamera.target.normalize();
+			}
+			lightCamera.target.x += ct.x;
+			lightCamera.target.y += ct.y;
+			lightCamera.target.z += ct.z;
+			lightCamera.pos.load(ct);
+			lightCamera.update();
+			for ( i in 0...lightCameras.length) {
+				if( ldir == null )
+					lightCameras[i].target.set(0, 0, -1);
+				else {
+					lightCameras[i].target.set(ldir.x, ldir.y, ldir.z);
+					lightCameras[i].target.normalize();
+				}
+				lightCameras[i].target.x += ct.x;
+				lightCameras[i].target.y += ct.y;
+				lightCameras[i].target.z += ct.z;
+				lightCameras[i].pos.load(ct);
+				lightCameras[i].update();
+			}
+		}
+
+		var textures = [];
+		if ( g != null )
+			g.clear();
+		for (i in 0...cascade) {
+			var texture = ctx.textures.allocTarget("cascadeShadowMap", size, size, false, format);
+			if( customDepth && (depth == null || depth.width != size || depth.height != size || depth.isDisposed()) ) {
+				if( depth != null ) depth.dispose();
+				depth = new h3d.mat.DepthBuffer(size, size);
+			}
+			texture.depthBuffer = depth;
+			textures.push(texture);
+
+			currentCascadeIndex = i;
+			var p = passes.save();
+			cullPasses(passes,function(col) return col.inFrustum(lightCameras[i].frustum));
+			processShadowMap( passes, texture, sort);
+			passes.load(p);
+			drawCascade(lightCameras[i], i);
+
+		}
+		drawCascade(lightCamera);
+		syncCascadeShader(textures);
+	}
+
+	var g : h3d.scene.Graphics;
+	function drawCascade( c : h3d.Camera, cascade=-1 ) {
+		if ( !debug )
+			return;
+
+		if( g == null ) {
+			g = new h3d.scene.Graphics(ctx.scene);
+			g.name = "frustumDebug";
+			g.material.mainPass.setPassName("overlay");
+			g.ignoreBounds = true;
+		}
+
+		var nearPlaneCorner = [c.unproject(-1, 1, 0), c.unproject(1, 1, 0), c.unproject(1, -1, 0), c.unproject(-1, -1, 0)];
+		var farPlaneCorner = [c.unproject(-1, 1, 1), c.unproject(1, 1, 1), c.unproject(1, -1, 1), c.unproject(-1, -1, 1)];
+
+		var colors = [0xffffff, 0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0x00ffff, 0xff00ff, 0x000000];
+		g.lineStyle(1, colors[cascade+1]);
+
+		// Near Plane
+		var last = nearPlaneCorner[nearPlaneCorner.length - 1];
+		g.moveTo(last.x,last.y,last.z);
+		for( fc in nearPlaneCorner ) {
+			g.lineTo(fc.x, fc.y, fc.z);
+		}
+
+		// Far Plane
+		var last = farPlaneCorner[farPlaneCorner.length - 1];
+		g.moveTo(last.x,last.y,last.z);
+		for( fc in farPlaneCorner ) {
+			g.lineTo(fc.x, fc.y, fc.z);
+		}
+
+		// Connections
+		for( i in 0 ... 4 ) {
+			var np = nearPlaneCorner[i];
+			var fp = farPlaneCorner[i];
+			g.moveTo(np.x, np.y, np.z);
+			g.lineTo(fp.x, fp.y, fp.z);
+		}
+	}
+}

+ 38 - 34
h3d/pass/DirShadowMap.hx

@@ -267,6 +267,38 @@ class DirShadowMap extends Shadows {
 		return true;
 	}
 
+	function processShadowMap( passes, tex, ?sort) {
+		ctx.engine.pushTarget(tex);
+		ctx.engine.clear(0xFFFFFF, 1);
+		super.draw(passes, sort);
+
+		var doBlur = blur.radius > 0 && (mode != Mixed || !ctx.computingStatic);
+
+		if( border != null && !doBlur )
+			border.render();
+
+		ctx.engine.popTarget();
+
+		if( mode == Mixed && !ctx.computingStatic ) {
+			var merge = ctx.textures.allocTarget("mergedDirShadowMap", size, size, false, format);
+			mergePass.shader.texA = tex;
+			mergePass.shader.texB = staticTexture;
+			ctx.engine.pushTarget(merge);
+			mergePass.render();
+			ctx.engine.popTarget();
+			tex = merge;
+		}
+
+		if( doBlur ) {
+			blur.apply(ctx, tex);
+			if( border != null ) {
+				ctx.engine.pushTarget(tex);
+				border.render();
+				ctx.engine.popTarget();
+			}
+		}
+	}
+
 	override function draw( passes, ?sort ) {
 		if( !enabled )
 			return;
@@ -282,13 +314,6 @@ class DirShadowMap extends Shadows {
 
 		cullPasses(passes,function(col) return col.inFrustum(lightCamera.frustum));
 
-		var texture = ctx.textures.allocTarget("dirShadowMap", size, size, false, format);
-		if( customDepth && (depth == null || depth.width != size || depth.height != size || depth.isDisposed()) ) {
-			if( depth != null ) depth.dispose();
-			depth = new h3d.mat.DepthBuffer(size, size);
-		}
-		texture.depthBuffer = depth;
-
 		if( mode != Mixed || ctx.computingStatic ) {
 			var ct = ctx.camera.target;
 			var slight = light == null ? ctx.lightSystem.shadowLight : light;
@@ -306,35 +331,14 @@ class DirShadowMap extends Shadows {
 			lightCamera.update();
 		}
 
-		ctx.engine.pushTarget(texture);
-		ctx.engine.clear(0xFFFFFF, 1);
-		super.draw(passes, sort);
-
-		var doBlur = blur.radius > 0 && (mode != Mixed || !ctx.computingStatic);
-
-		if( border != null && !doBlur )
-			border.render();
-
-		ctx.engine.popTarget();
-
-		if( mode == Mixed && !ctx.computingStatic ) {
-			var merge = ctx.textures.allocTarget("mergedDirShadowMap", size, size, false, format);
-			mergePass.shader.texA = texture;
-			mergePass.shader.texB = staticTexture;
-			ctx.engine.pushTarget(merge);
-			mergePass.render();
-			ctx.engine.popTarget();
-			texture = merge;
+		var texture = ctx.textures.allocTarget("dirShadowMap", size, size, false, format);
+		if( customDepth && (depth == null || depth.width != size || depth.height != size || depth.isDisposed()) ) {
+			if( depth != null ) depth.dispose();
+			depth = new h3d.mat.DepthBuffer(size, size);
 		}
+		texture.depthBuffer = depth;
 
-		if( doBlur ) {
-			blur.apply(ctx, texture);
-			if( border != null ) {
-				ctx.engine.pushTarget(texture);
-				border.render();
-				ctx.engine.popTarget();
-			}
-		}
+		processShadowMap(passes, texture, sort);
 
 		syncShader(texture);
 	}

+ 2 - 2
h3d/scene/pbr/DirLight.hx

@@ -4,9 +4,9 @@ class DirLight extends Light {
 
 	var pbr : h3d.shader.pbr.Light.DirLight;
 
-	public function new(?dir: h3d.Vector, ?parent) {
+	public function new(?dir: h3d.Vector, ?parent, ?cascade) {
 		pbr = new h3d.shader.pbr.Light.DirLight();
-		shadows = new h3d.pass.DirShadowMap(this);
+		shadows = cascade ? new h3d.pass.CascadeShadowMap(this) : new h3d.pass.DirShadowMap(this);
 		super(pbr,parent);
 		if( dir != null ) setDirection(dir);
 	}

+ 64 - 0
h3d/shader/CascadeShadow.hx

@@ -0,0 +1,64 @@
+package h3d.shader;
+
+class CascadeShadow extends DirShadow {
+
+	static var SRC = {
+
+		@const(5) var CASCADE_COUNT:Int;
+		@param var cascadeShadowMaps : Array<Sampler2D, CASCADE_COUNT>;
+		@param var cascadeProjs : Array<Mat3x4, CASCADE_COUNT>;
+		@param var cascadeLimits : Array<Float, CASCADE_COUNT>;
+		@param var camViewProj : Mat4;
+
+		function fragment() {
+			if( enable ) {
+				var camProjectedPosition = vec4(transformedPosition, 1) * camViewProj;
+				var index = 0;
+				@unroll for ( i in 0...CASCADE_COUNT-1 ) {
+					if ( camProjectedPosition.z > cascadeLimits[i] )
+				 		index = i + 1;
+				}
+				if( USE_PCF ) {
+					shadow = 1.0;
+					var texelSize = 1.0/shadowRes;
+					@unroll for ( c in 0...CASCADE_COUNT ) {
+						var shadowPos = transformedPosition * cascadeProjs[c];
+						var zMax = shadowPos.z.saturate();
+						var shadowUv = screenToUv(shadowPos.xy);
+
+						var rot = rand(transformedPosition.x + transformedPosition.y + transformedPosition.z) * 3.14 * 2;
+						var cosR = cos(rot);
+						var sinR = sin(rot);
+						var sampleStrength = 1.0 / PCF_SAMPLES;
+						var offScale = texelSize * pcfScale;
+						for(i in 0...PCF_SAMPLES) {
+							var offset = poissonDisk[i].xy * offScale;
+							offset = vec2(cosR * offset.x - sinR * offset.y, cosR * offset.y + sinR * offset.x);
+							var depth = cascadeShadowMaps[c].getLod(shadowUv + offset, 0).r;
+							shadow  -= (zMax - shadowBias > depth) ? sampleStrength : 0.0;
+						}
+					}
+				}
+				else if( USE_ESM ) {
+					var shadowPos = transformedPosition * cascadeProjs[0];
+					var zMax = shadowPos.z.saturate();
+					var depth = cascadeShadowMaps[0].get(screenToUv(shadowPos.xy)).r;
+					var delta = (depth + shadowBias).min(zMax) - zMax;
+					shadow = exp(shadowPower * delta).saturate();
+				}
+				else {
+					shadow = 1.0;
+					@unroll for ( c in 0...CASCADE_COUNT ) {
+						var shadowPos = transformedPosition * cascadeProjs[c];
+						var shadowUv = screenToUv(shadowPos.xy);
+						var zMax = shadowPos.z.saturate();
+						var depth = cascadeShadowMaps[c].get(shadowUv).r;
+						shadow -= zMax - shadowBias > depth ? 1 : 0;
+					}
+				}
+			}
+			shadow = saturate(shadow);
+			dirShadow = shadow;
+		}
+	}
+}