Browse Source

Adding CapsuleLight. Capsule lights use same shadow mapping process as point light for now so I created a CubeShadowMap class.

clementlandrin 1 year ago
parent
commit
a21c40b6b8

+ 63 - 0
h3d/pass/CapsuleShadowMap.hx

@@ -0,0 +1,63 @@
+package h3d.pass;
+
+class CapsuleShadowMap extends CubeShadowMap {
+
+	var pshader : h3d.shader.PointShadow;
+
+	public function new( light : h3d.scene.Light, useWorldDist : Bool ) {
+		super(light, useWorldDist);
+		shader = pshader = new h3d.shader.PointShadow();
+	}
+
+	override function set_mode(m:Shadows.RenderMode) {
+		pshader.enable = m != None && enabled;
+		return mode = m;
+	}
+
+	override function set_enabled(b:Bool) {
+		pshader.enable = b && mode != None;
+		return enabled = b;
+	}
+
+	override function getShadowTex() {
+		return pshader.shadowMap;
+	}
+
+	override function syncShader(texture) {
+		if( texture == null )
+			throw "assert";
+		var capsuleLight = cast(light, h3d.scene.pbr.CapsuleLight);
+		pshader.shadowMap = texture;
+		pshader.shadowBias = bias;
+		pshader.shadowPower = power;
+		pshader.lightPos = light.getAbsPos().getPosition();
+		pshader.zFar = capsuleLight.range + capsuleLight.length;
+
+		// ESM
+		pshader.USE_ESM = samplingKind == ESM;
+		pshader.shadowPower = power;
+
+		// PCF
+		pshader.USE_PCF = samplingKind == PCF;
+		pshader.pcfScale = pcfScale / 100.0;
+		pshader.pcfQuality = pcfQuality;
+	}
+
+	override function createCollider(light : h3d.scene.Light) {
+		var absPos = light.getAbsPos();
+		var capsuleLight = cast(light, h3d.scene.pbr.CapsuleLight);
+		// TODO : Optimize culling
+		return new h3d.col.Sphere(absPos.tx, absPos.ty, absPos.tz, capsuleLight.range + capsuleLight.length * 0.5);
+	}
+
+	override function cull(lightCollider, col) {
+		var sphere = cast(lightCollider, h3d.col.Sphere);
+		return col.inSphere(sphere);
+	}
+
+	override function updateLightCameraNearFar(light : h3d.scene.Light) {
+		var capsuleLight = cast(light, h3d.scene.pbr.CapsuleLight);
+		lightCamera.zFar = capsuleLight.range;
+		lightCamera.zNear = capsuleLight.zNear;
+	}
+}

+ 245 - 0
h3d/pass/CubeShadowMap.hx

@@ -0,0 +1,245 @@
+package h3d.pass;
+
+enum CubeFaceFlag {
+	Right;
+	Left;
+	Back;
+	Front;
+	Top;
+	Bottom;
+}
+
+class CubeShadowMap extends Shadows {
+
+	var depth : h3d.mat.Texture;
+	var mergePass = new h3d.pass.ScreenFx(new h3d.shader.MinMaxShader.CubeMinMaxShader());
+	public var faceMask(default, null) : haxe.EnumFlags<CubeFaceFlag>;
+
+	var cubeDir = [ h3d.Matrix.L([0,0,-1,0, 0,-1,0,0, 1,0,0,0]),
+					h3d.Matrix.L([0,0,1,0, 0,-1,0,0, -1,0,0,0]),
+	 				h3d.Matrix.L([1,0,0,0, 0,0,1,0, 0,1,0,0]),
+	 				h3d.Matrix.L([1,0,0,0, 0,0,-1,0, 0,-1,0,0]),
+				 	h3d.Matrix.L([1,0,0,0, 0,-1,0,0, 0,0,1,0]),
+				 	h3d.Matrix.L([-1,0,0,0, 0,-1,0,0, 0,0,-1,0]) ];
+
+	public function new( light : h3d.scene.Light, useWorldDist : Bool ) {
+		super(light);
+		lightCamera = new h3d.Camera();
+		lightCamera.screenRatio = 1.0;
+		lightCamera.fovY = 90;
+
+		faceMask.set(Front);
+		faceMask.set(Back);
+		faceMask.set(Top);
+		faceMask.set(Bottom);
+		faceMask.set(Left);
+		faceMask.set(Right);
+	}
+
+	override function set_size(s) {
+		return super.set_size(s);
+	}
+
+	override function dispose() {
+		super.dispose();
+		if( depth != null ) depth.dispose();
+		if( tmpTex != null) tmpTex.dispose();
+	}
+
+	override function isUsingWorldDist(){
+		return true;
+	}
+
+	override function setGlobals() {
+		super.setGlobals();
+		cameraViewProj = getShadowProj();
+		cameraFar = lightCamera.zFar;
+		cameraPos = lightCamera.pos;
+	}
+
+	override function saveStaticData() {
+		if( mode != Mixed && mode != Static )
+			return null;
+		if( staticTexture == null )
+			throw "Data not computed";
+
+		var buffer = new haxe.io.BytesBuffer();
+		buffer.addInt32(staticTexture.width);
+
+		for(i in 0 ... 6){
+			var bytes = haxe.zip.Compress.run(staticTexture.capturePixels(i).bytes,9);
+			buffer.addInt32(bytes.length);
+			buffer.add(bytes);
+		}
+
+		return buffer.getBytes();
+	}
+
+	function createStaticTexture() : h3d.mat.Texture {
+		if( staticTexture != null && staticTexture.width == size && staticTexture.width == size && staticTexture.format == format )
+			return staticTexture;
+		if( staticTexture != null )
+			staticTexture.dispose();
+		staticTexture = new h3d.mat.Texture(size, size, [Target, Cube], format);
+		staticTexture.name = "staticTexture";
+		staticTexture.preventAutoDispose();
+		staticTexture.realloc = function () {
+			if( pixelsForRealloc != null && pixelsForRealloc.length == 6 ) {
+				for( i in 0 ... 6 ) {
+					var pixels = pixelsForRealloc[i];
+					staticTexture.uploadPixels(pixels, 0, i);
+				}
+			}
+		}
+		return staticTexture;
+	}
+
+	var pixelsForRealloc : Array<hxd.Pixels> = null;
+	override function loadStaticData( bytes : haxe.io.Bytes ) {
+		if( (mode != Mixed && mode != Static) || bytes == null || bytes.length == 0 )
+			return false;
+		var buffer = new haxe.io.BytesInput(bytes);
+		var size = buffer.readInt32();
+		if( size != this.size )
+			return false;
+
+		createStaticTexture();
+
+		pixelsForRealloc = [];
+		for( i in 0 ... 6 ) {
+			var len = buffer.readInt32();
+			var pixels = new hxd.Pixels(size, size, haxe.zip.Uncompress.run(buffer.read(len)), format);
+			pixelsForRealloc.push(pixels);
+			staticTexture.uploadPixels(pixels, 0, i);
+		}
+		syncShader(staticTexture);
+
+		return true;
+	}
+
+	var tmpTex : h3d.mat.Texture;
+	override function createDefaultShadowMap() {
+		if( tmpTex != null) return tmpTex;
+		if ( mode == Mixed )
+			tmpTex = new h3d.mat.Texture(size,size, [Target,Cube], format);
+		else
+			tmpTex = new h3d.mat.Texture(1,1, [Target,Cube], format);
+		tmpTex.name = "defaultCubeShadowMap";
+		tmpTex.realloc = function() clear(tmpTex);
+		clear(tmpTex);
+		return tmpTex;
+	}
+
+	inline function clear( t : h3d.mat.Texture, ?layer = -1 ) {
+		if( format == RGBA )
+			t.clear(0xFFFFFF, layer);
+		else
+			t.clearF(1, 1, 1, 1, layer);
+	}
+
+	function updateLightCameraNearFar(light : h3d.scene.Light) {
+	}
+
+	function createCollider(light : h3d.scene.Light) : h3d.col.Collider {
+		return null;
+	}
+
+	function cull(lightCollider : h3d.col.Collider, col : h3d.col.Collider) {
+		return false;
+	}
+
+	var clearDepthColor = new h3d.Vector(1,1,1,1);
+	override function draw( passes : h3d.pass.PassList, ?sort ) {
+		if( !enabled )
+			return;
+
+		if( !filterPasses(passes) )
+			return;
+
+		if( passes.isEmpty() ) {
+			syncShader(staticTexture == null ? createDefaultShadowMap() : staticTexture);
+			return;
+		}
+
+		var lightCollider = createCollider(light);
+		cullPasses(passes,function(col) return cull(lightCollider, col));
+
+		if( passes.isEmpty() ) {
+			syncShader(staticTexture == null ? createDefaultShadowMap() : staticTexture);
+			return;
+		}
+
+		var texture = ctx.computingStatic ? createStaticTexture() : ctx.textures.allocTarget("pointShadowMap", size, size, false, format, true);
+		if( depth == null || depth.width != texture.width || depth.height != texture.height || depth.isDisposed() ) {
+			if( depth != null ) depth.dispose();
+			depth = new h3d.mat.Texture(texture.width, texture.height, Depth24Stencil8);
+		}
+		texture.depthBuffer = depth;
+
+		var absPos = light.getAbsPos();
+		lightCamera.pos.set(absPos.tx, absPos.ty, absPos.tz);
+		updateLightCameraNearFar(light);
+		
+
+		for( i in 0...6 ) {
+
+			// Shadows on the current face is disabled
+			if( !faceMask.has(CubeFaceFlag.createByIndex(i)) ) {
+				clear(texture, i);
+				continue;
+			}
+
+			lightCamera.setCubeMap(i);
+			lightCamera.update();
+
+			var save = passes.save();
+			cullPasses(passes, function(col) return col.inFrustum(lightCamera.frustum));
+			if( passes.isEmpty() ) {
+				passes.load(save);
+				clear(texture, i);
+				continue;
+			}
+
+			ctx.engine.pushTarget(texture, i);
+			format == RGBA ? ctx.engine.clear(0xFFFFFF, i) : ctx.engine.clearF(clearDepthColor, 1);
+			super.draw(passes,sort);
+			passes.load(save);
+			ctx.engine.popTarget();
+		}
+
+		// Blur is applied even if there's no shadows - TO DO : remove the useless blur pass
+		if( blur.radius > 0 )
+			blur.apply(ctx, texture);
+
+		if( mode == Mixed && !ctx.computingStatic )
+			syncShader(merge(texture));
+		else
+			syncShader(texture);
+	}
+
+	function merge( dynamicTex : h3d.mat.Texture ) : h3d.mat.Texture{
+		var validBakedTexture = (staticTexture != null && staticTexture.width == dynamicTex.width);
+		var merge : h3d.mat.Texture = null;
+		if( mode == Mixed && !ctx.computingStatic && validBakedTexture)
+			merge = ctx.textures.allocTarget("mergedPointShadowMap", size, size, false, format, true);
+
+		if( mode == Mixed && !ctx.computingStatic && merge != null ) {
+			for( i in 0 ... 6 ) {
+				if( !faceMask.has(CubeFaceFlag.createByIndex(i)) ) continue;
+				mergePass.shader.texA = dynamicTex;
+				mergePass.shader.texB = staticTexture;
+				mergePass.shader.mat = cubeDir[i];
+				ctx.engine.pushTarget(merge, i);
+				mergePass.render();
+				ctx.engine.popTarget();
+			}
+		}
+		return merge;
+	}
+
+	override function computeStatic( passes : h3d.pass.PassList ) {
+		if( mode != Static && mode != Mixed )
+			return;
+		draw(passes);
+	}
+}

+ 10 - 219
h3d/pass/PointShadowMap.hx

@@ -1,41 +1,12 @@
 package h3d.pass;
 package h3d.pass;
 
 
-enum CubeFaceFlag {
-	Right;
-	Left;
-	Back;
-	Front;
-	Top;
-	Bottom;
-}
+class PointShadowMap extends CubeShadowMap {
 
 
-class PointShadowMap extends Shadows {
-
-	var depth : h3d.mat.Texture;
 	var pshader : h3d.shader.PointShadow;
 	var pshader : h3d.shader.PointShadow;
-	var mergePass = new h3d.pass.ScreenFx(new h3d.shader.MinMaxShader.CubeMinMaxShader());
-	public var faceMask(default, null) : haxe.EnumFlags<CubeFaceFlag>;
-
-	var cubeDir = [ h3d.Matrix.L([0,0,-1,0, 0,-1,0,0, 1,0,0,0]),
-					h3d.Matrix.L([0,0,1,0, 0,-1,0,0, -1,0,0,0]),
-	 				h3d.Matrix.L([1,0,0,0, 0,0,1,0, 0,1,0,0]),
-	 				h3d.Matrix.L([1,0,0,0, 0,0,-1,0, 0,-1,0,0]),
-				 	h3d.Matrix.L([1,0,0,0, 0,-1,0,0, 0,0,1,0]),
-				 	h3d.Matrix.L([-1,0,0,0, 0,-1,0,0, 0,0,-1,0]) ];
 
 
 	public function new( light : h3d.scene.Light, useWorldDist : Bool ) {
 	public function new( light : h3d.scene.Light, useWorldDist : Bool ) {
-		super(light);
-		lightCamera = new h3d.Camera();
-		lightCamera.screenRatio = 1.0;
-		lightCamera.fovY = 90;
+		super(light, useWorldDist);
 		shader = pshader = new h3d.shader.PointShadow();
 		shader = pshader = new h3d.shader.PointShadow();
-
-		faceMask.set(Front);
-		faceMask.set(Back);
-		faceMask.set(Top);
-		faceMask.set(Bottom);
-		faceMask.set(Left);
-		faceMask.set(Right);
 	}
 	}
 
 
 	override function set_mode(m:Shadows.RenderMode) {
 	override function set_mode(m:Shadows.RenderMode) {
@@ -48,31 +19,10 @@ class PointShadowMap extends Shadows {
 		return enabled = b;
 		return enabled = b;
 	}
 	}
 
 
-	override function set_size(s) {
-		return super.set_size(s);
-	}
-
-	override function dispose() {
-		super.dispose();
-		if( depth != null ) depth.dispose();
-		if( tmpTex != null) tmpTex.dispose();
-	}
-
-	override function isUsingWorldDist(){
-		return true;
-	}
-
 	override function getShadowTex() {
 	override function getShadowTex() {
 		return pshader.shadowMap;
 		return pshader.shadowMap;
 	}
 	}
 
 
-	override function setGlobals() {
-		super.setGlobals();
-		cameraViewProj = getShadowProj();
-		cameraFar = lightCamera.zFar;
-		cameraPos = lightCamera.pos;
-	}
-
 	override function syncShader(texture) {
 	override function syncShader(texture) {
 		if( texture == null )
 		if( texture == null )
 			throw "assert";
 			throw "assert";
@@ -93,179 +43,20 @@ class PointShadowMap extends Shadows {
 		pshader.pcfQuality = pcfQuality;
 		pshader.pcfQuality = pcfQuality;
 	}
 	}
 
 
-	override function saveStaticData() {
-		if( mode != Mixed && mode != Static )
-			return null;
-		if( staticTexture == null )
-			throw "Data not computed";
-
-		var buffer = new haxe.io.BytesBuffer();
-		buffer.addInt32(staticTexture.width);
-
-		for(i in 0 ... 6){
-			var bytes = haxe.zip.Compress.run(staticTexture.capturePixels(i).bytes,9);
-			buffer.addInt32(bytes.length);
-			buffer.add(bytes);
-		}
-
-		return buffer.getBytes();
-	}
-
-	function createStaticTexture() : h3d.mat.Texture {
-		if( staticTexture != null && staticTexture.width == size && staticTexture.width == size && staticTexture.format == format )
-			return staticTexture;
-		if( staticTexture != null )
-			staticTexture.dispose();
-		staticTexture = new h3d.mat.Texture(size, size, [Target, Cube], format);
-		staticTexture.name = "staticTexture";
-		staticTexture.preventAutoDispose();
-		staticTexture.realloc = function () {
-			if( pixelsForRealloc != null && pixelsForRealloc.length == 6 ) {
-				for( i in 0 ... 6 ) {
-					var pixels = pixelsForRealloc[i];
-					staticTexture.uploadPixels(pixels, 0, i);
-				}
-			}
-		}
-		return staticTexture;
-	}
-
-	var pixelsForRealloc : Array<hxd.Pixels> = null;
-	override function loadStaticData( bytes : haxe.io.Bytes ) {
-		if( (mode != Mixed && mode != Static) || bytes == null || bytes.length == 0 )
-			return false;
-		var buffer = new haxe.io.BytesInput(bytes);
-		var size = buffer.readInt32();
-		if( size != this.size )
-			return false;
-
-		createStaticTexture();
-
-		pixelsForRealloc = [];
-		for( i in 0 ... 6 ) {
-			var len = buffer.readInt32();
-			var pixels = new hxd.Pixels(size, size, haxe.zip.Uncompress.run(buffer.read(len)), format);
-			pixelsForRealloc.push(pixels);
-			staticTexture.uploadPixels(pixels, 0, i);
-		}
-		syncShader(staticTexture);
-
-		return true;
-	}
-
-	var tmpTex : h3d.mat.Texture;
-	override function createDefaultShadowMap() {
-		if( tmpTex != null) return tmpTex;
-		if ( mode == Mixed )
-			tmpTex = new h3d.mat.Texture(size,size, [Target,Cube], format);
-		else
-			tmpTex = new h3d.mat.Texture(1,1, [Target,Cube], format);
-		tmpTex.name = "defaultCubeShadowMap";
-		tmpTex.realloc = function() clear(tmpTex);
-		clear(tmpTex);
-		return tmpTex;
+	override function createCollider(light : h3d.scene.Light) {
+		var absPos = light.getAbsPos();
+		var pointLight = cast(light, h3d.scene.pbr.PointLight);
+		return new h3d.col.Sphere(absPos.tx, absPos.ty, absPos.tz, pointLight.range);
 	}
 	}
 
 
-	inline function clear( t : h3d.mat.Texture, ?layer = -1 ) {
-		if( format == RGBA )
-			t.clear(0xFFFFFF, layer);
-		else
-			t.clearF(1, 1, 1, 1, layer);
+	override function cull(lightCollider, col) {
+		var sphere = cast(lightCollider, h3d.col.Sphere);
+		return col.inSphere(sphere);
 	}
 	}
 
 
-	var clearDepthColor = new h3d.Vector(1,1,1,1);
-	override function draw( passes : h3d.pass.PassList, ?sort ) {
-		if( !enabled )
-			return;
-
-		if( !filterPasses(passes) )
-			return;
-
-		if( passes.isEmpty() ) {
-			syncShader(staticTexture == null ? createDefaultShadowMap() : staticTexture);
-			return;
-		}
-
+	override function updateLightCameraNearFar(light : h3d.scene.Light) {
 		var pointLight = cast(light, h3d.scene.pbr.PointLight);
 		var pointLight = cast(light, h3d.scene.pbr.PointLight);
-		var absPos = light.getAbsPos();
-		var sp = new h3d.col.Sphere(absPos.tx, absPos.ty, absPos.tz, pointLight.range);
-		cullPasses(passes,function(col) return col.inSphere(sp));
-
-		if( passes.isEmpty() ) {
-			syncShader(staticTexture == null ? createDefaultShadowMap() : staticTexture);
-			return;
-		}
-
-		var texture = ctx.computingStatic ? createStaticTexture() : ctx.textures.allocTarget("pointShadowMap", size, size, false, format, true);
-		if( depth == null || depth.width != texture.width || depth.height != texture.height || depth.isDisposed() ) {
-			if( depth != null ) depth.dispose();
-			depth = new h3d.mat.Texture(texture.width, texture.height, Depth24Stencil8);
-		}
-		texture.depthBuffer = depth;
-
-		lightCamera.pos.set(absPos.tx, absPos.ty, absPos.tz);
 		lightCamera.zFar = pointLight.range;
 		lightCamera.zFar = pointLight.range;
 		lightCamera.zNear = pointLight.zNear;
 		lightCamera.zNear = pointLight.zNear;
-
-		for( i in 0...6 ) {
-
-			// Shadows on the current face is disabled
-			if( !faceMask.has(CubeFaceFlag.createByIndex(i)) ) {
-				clear(texture, i);
-				continue;
-			}
-
-			lightCamera.setCubeMap(i);
-			lightCamera.update();
-
-			var save = passes.save();
-			cullPasses(passes, function(col) return col.inFrustum(lightCamera.frustum));
-			if( passes.isEmpty() ) {
-				passes.load(save);
-				clear(texture, i);
-				continue;
-			}
-
-			ctx.engine.pushTarget(texture, i);
-			format == RGBA ? ctx.engine.clear(0xFFFFFF, i) : ctx.engine.clearF(clearDepthColor, 1);
-			super.draw(passes,sort);
-			passes.load(save);
-			ctx.engine.popTarget();
-		}
-
-		// Blur is applied even if there's no shadows - TO DO : remove the useless blur pass
-		if( blur.radius > 0 )
-			blur.apply(ctx, texture);
-
-		if( mode == Mixed && !ctx.computingStatic )
-			syncShader(merge(texture));
-		else
-			syncShader(texture);
-	}
-
-	function merge( dynamicTex : h3d.mat.Texture ) : h3d.mat.Texture{
-		var validBakedTexture = (staticTexture != null && staticTexture.width == dynamicTex.width);
-		var merge : h3d.mat.Texture = null;
-		if( mode == Mixed && !ctx.computingStatic && validBakedTexture)
-			merge = ctx.textures.allocTarget("mergedPointShadowMap", size, size, false, format, true);
-
-		if( mode == Mixed && !ctx.computingStatic && merge != null ) {
-			for( i in 0 ... 6 ) {
-				if( !faceMask.has(CubeFaceFlag.createByIndex(i)) ) continue;
-				mergePass.shader.texA = dynamicTex;
-				mergePass.shader.texB = staticTexture;
-				mergePass.shader.mat = cubeDir[i];
-				ctx.engine.pushTarget(merge, i);
-				mergePass.render();
-				ctx.engine.popTarget();
-			}
-		}
-		return merge;
-	}
-
-	override function computeStatic( passes : h3d.pass.PassList ) {
-		if( mode != Static && mode != Mixed )
-			return;
-		draw(passes);
 	}
 	}
 }
 }

+ 100 - 0
h3d/scene/pbr/CapsuleLight.hx

@@ -0,0 +1,100 @@
+package h3d.scene.pbr;
+
+class CapsuleLight extends Light {
+
+	var pbr : h3d.shader.pbr.Light.CapsuleLight;
+	public var radius : Float = 0.5;
+	public var length(default, set) : Float = 1.0;
+	public var zNear : Float = 0.02;
+	/**
+		Alias for uniform scale.
+	**/
+	public var range(get,set) : Float;
+
+	public function new(?parent) {
+		pbr = new h3d.shader.pbr.Light.CapsuleLight();
+		shadows = new h3d.pass.CapsuleShadowMap(this, true);
+		super(pbr,parent);
+		range = 10;
+	}
+
+	public override function clone( ?o : h3d.scene.Object ) : h3d.scene.Object {
+		var cl = o == null ? new CapsuleLight(null) : cast o;
+		super.clone(cl);
+		cl.radius = radius;
+		cl.length = length;
+		cl.range = range;
+		return cl;
+	}
+
+	function get_range() {
+		var minScale = 1.0;
+		var p = parent;
+		while (p != null) {
+			minScale *= hxd.Math.min(p.scaleX, hxd.Math.min(p.scaleY, p.scaleZ));
+			p = p.parent;
+		}
+		return scaleX * minScale;
+	}
+
+	function updatePrim() {
+		if ( primitive != null )
+			primitive.dispose();
+		primitive = new h3d.prim.Capsule(1.0, length / scaleX, 16);
+	}
+
+	function set_range(v:Float) {
+		setScale(v);
+		updatePrim();
+		return v;
+	}
+
+	function set_length(v:Float) {
+		length = v;
+		updatePrim();
+		return length;
+	}
+
+	override function draw(ctx:RenderContext) {
+		primitive.render(ctx.engine);
+	}
+
+	override function sync(ctx) {
+		super.sync(ctx);
+
+		pbr.lightColor.load(_color);
+		var range = hxd.Math.max(range, 1e-10);
+		var power = power * 10; // base scale
+		pbr.lightColor.scale(power * power);
+		pbr.lightPos.set(absPos.getPosition().x, absPos.getPosition().y, absPos.getPosition().z);
+		pbr.radius = radius;
+		pbr.halfLength = length * 0.5;
+		pbr.occlusionFactor = occlusionFactor;
+		pbr.left = absPos.front();
+		var d = range - radius;
+		pbr.invRange4 = 1 / (d * d * d * d);
+	}
+
+	var s = new h3d.col.Sphere();
+	override function emit(ctx:RenderContext) {
+		if( ctx.computingStatic ) {
+			super.emit(ctx);
+			return;
+		}
+
+		if( ctx.pbrLightPass == null )
+			throw "Rendering a pbr light require a PBR compatible scene renderer";
+
+		s.x = absPos._41;
+		s.y = absPos._42;
+		s.z = absPos._43;
+		// TODO optimize culling
+		s.r = range + length;
+
+		if( !ctx.camera.frustum.hasSphere(s) )
+			return;
+
+		super.emit(ctx);
+		ctx.emitPass(ctx.pbrLightPass, this);
+	}
+}

+ 59 - 0
h3d/shader/pbr/Light.hx

@@ -46,6 +46,7 @@ class Light extends LightEvaluation {
 	static var SRC = {
 	static var SRC = {
 
 
 		var pbrLightDirection : Vec3;
 		var pbrLightDirection : Vec3;
+		var pbrSpecularLightDirection : Vec3;
 		var pbrLightColor : Vec3;
 		var pbrLightColor : Vec3;
 		var pbrOcclusionFactor : Float;
 		var pbrOcclusionFactor : Float;
 		var transformedPosition : Vec3;
 		var transformedPosition : Vec3;
@@ -138,3 +139,61 @@ class Performance extends hxsl.Shader {
 		}
 		}
 	}
 	}
 }
 }
+
+class CapsuleLight extends Light {
+
+	static var SRC = {
+		var normal : Vec3;
+
+		@param var lightPos : Vec3;
+		@param var radius : Float;
+		@param var invRange4 : Float;
+		@param var halfLength : Float;
+		@param var left : Vec3;
+
+
+		function closestPointOnLine(a : Vec3, b : Vec3 , c : Vec3) : Vec3 {
+			var ab = b - a;
+			var t = dot(c - a, ab) / dot(ab, ab);
+			return a + t * ab ;
+		}
+
+		function closestPointOnSegment( a : Vec3, b : Vec3, c : Vec3) : Vec3 {
+			var ab = b - a;
+			var t = dot(c - a, ab) / dot(ab, ab);
+			return a + saturate(t) * ab;
+		}
+
+		var pixelColor : Vec4;
+		var view : Vec3;
+		function fragment() {
+			var P0 = lightPos - halfLength * left;
+			var P1 = lightPos + halfLength * left;
+
+			// Diffuse: place a point light on the closest point on the sphere placed on the closest position on the segment.
+			var spherePos = closestPointOnSegment(P0, P1, transformedPosition);
+			var delta = spherePos - transformedPosition;
+			pbrLightDirection = delta.normalize();
+			var closestPointDiffuse = spherePos - pbrLightDirection * saturate((length(delta) - 1e-5) / radius) * radius;
+			delta = closestPointDiffuse - transformedPosition;
+			pbrLightDirection = normalize(delta);
+
+			// Attenuation.
+			var falloff = pointLightIntensity(delta, radius, invRange4);
+
+			// Specular.
+			var R = view - 2.0 * dot(view, normal) * normal;
+			var posToLight = lightPos - transformedPosition;
+			// Intersect a light plane with reflected ray and place a sphere on the closest point on segment.
+			var onPlane = transformedPosition + R * dot(posToLight, R);
+			var spherePosSpec = closestPointOnSegment(P0, P1, onPlane);
+			pbrSpecularLightDirection = normalize(spherePosSpec - transformedPosition);
+			// Get closest point on the sphere.
+			var closestPointSpecular = spherePosSpec - pbrSpecularLightDirection * saturate(length(spherePosSpec - transformedPosition) - 1e-5 / radius) * radius;
+			pbrSpecularLightDirection = normalize(closestPointSpecular - transformedPosition);
+
+			pbrLightColor = falloff * lightColor;
+			pbrOcclusionFactor = occlusionFactor;
+		}
+	};
+}