Capsule light functionality added in lightmass

Hey all, thought I’d share a little something I did, after discovering that capsule lights weren’t supported in lightmass. The you can find the original topic here:

I’ve already submitted a pull request to the main branch on github, but if you can’t wait for that, read on.

Basically, the issue was this:


…and the fix looks like this:


So, on to the code. To implement this, you’ll need to touch 4 files. They are:
Engine/Source/Programs/UnrealLightmass/Public/SceneExport.h
Engine/Source/Editor/UnrealEd/Private/Lightmass/Lightmass.cpp
Engine/Source/Programs/UnrealLightmass/Private/ImportExport/LightmassScene.h
Engine/Source/Programs/UnrealLightmass/Private/ImportExport/LightmassScene.cpp

(note: this is done on version 4.1.1, so if you’re on a another version, things could be slightly different, so it’s a good idea to follow along and manually change the code that needs to change. I’ve also uploaded the four files just incase you have a hard time following along)

We need to start by passing the length of our light into lightmass so we can work with it. In SceneExport.h, locate the struct name “FLightData”. Add a float variable named LightSourceLength.


struct FLightData
{
	FGuid		Guid;
	/** Bit-wise combination of flags from EDawnLightFlags */
	uint32		LightFlags;
	/** Homogeneous coordinates */
	FVector4	Position;
	FVector4	Direction;
	FColor		Color;
	float		Brightness;
	/** The radius of the light's surface, not the light's influence. */
	float		LightSourceRadius;
	/** The length of the light source*/
	float		LightSourceLength;
	/** Scale factor for the indirect lighting */
	float		IndirectLightingScale;

Now we need to fill our that variable with the right data. jump over to Lightmass.cpp and find the “WriteLights” function. You’ll see a for loop for each of the light types. In the for loop for the directional light, set the LightData.LightSourceLength to 0, since it doesn’t make sense to have a length. In the point and spot light loops, set the light source from the data stored in the light actor. The code should look like this:

In the directional light loop:


		LightData.ShadowExponent = Light->LightmassSettings.ShadowExponent;
		LightData.LightSourceRadius = 0;
		LightData.LightSourceLength = 0;
		DirectionalData.LightSourceAngle = Light->LightmassSettings.LightSourceAngle * (float)PI / 180.0f;

In both the point and spot light loops:


		LightData.ShadowExponent = Light->LightmassSettings.ShadowExponent;
		LightData.LightSourceRadius = Light->SourceRadius;
		LightData.LightSourceLength = Light->SourceLength;
		DirectionalData.LightSourceAngle = Light->LightmassSettings.LightSourceAngle * (float)PI / 180.0f;

We now have the length of the light being passed to lightmass, so we can move on to implementing the actual functionality in LightmassScene.cpp. Find and replace the following functions to have lightmass use the light length when calculating point lights. I tried to be fairly descriptive with the comments, but it’s relatively complex vector math.


/**
 * Computes the intensity of the direct lighting from this light on a specific point.
 */
FLinearColor FPointLight::GetDirectIntensity(const FVector4& Point, bool bCalculateForIndirectLighting) const
{
	if (LightFlags & GI_LIGHT_INVERSE_SQUARED)
	{
		float DistanceSqr = 0;
		// If our light has a length, find the closest point along that length, to our point.
		if (LightSourceLength > 0)
			DistanceSqr = (GetClosetPointToCapsuleLight(Point) - Point).SizeSquared3();
		else
			DistanceSqr = (Position - Point).SizeSquared3();

		float DistanceAttenuation = 16.0f / (DistanceSqr + 0.0001f);

		float LightRadiusMask = FMath::Square(FMath::Max(0.0f, 1.0f - FMath::Square(DistanceSqr / (Radius * Radius))));
		DistanceAttenuation *= LightRadiusMask;

		return FLight::GetDirectIntensity(Point, bCalculateForIndirectLighting) * DistanceAttenuation;
	}
	else
	{
		float RadialAttenuation = FMath::Pow(FMath::Max(1.0f - ((Position - Point) / Radius).SizeSquared3(), 0.0f), FalloffExponent);

		return FLight::GetDirectIntensity(Point, bCalculateForIndirectLighting) * RadialAttenuation;
	}
}


/** Gets a single direction to use for direct lighting that is representative of the whole area light. */
FVector4 FPointLight::GetDirectLightingDirection(const FVector4& Point, const FVector4& PointNormal) const
{
	FVector4 LightPosition = Position;

	if (LightSourceLength >= 0)
		LightPosition = GetClosetPointToCapsuleLight(Point);

	// The position on the point light surface sphere that will first be visible to a triangle rotating toward the light
	const FVector4 FirstVisibleLightPoint = LightPosition + PointNormal * LightSourceRadius;
	return FirstVisibleLightPoint - Point;
}


/** Generates a sample on the light's surface. */
void FPointLight::SampleLightSurface(FLMRandomStream& RandomStream, FLightSurfaceSample& Sample) const
{
	Sample.DiskPosition = FVector2D(0, 0);

	if (LightSourceLength <= 0)
	{
		// Generate a sample on the surface of the sphere with uniform density over the surface area of the sphere
		//@todo - stratify
		const FVector4 UnitSpherePosition = GetUnitVector(RandomStream);
		Sample.Position = UnitSpherePosition * LightSourceRadius + Position;
		Sample.Normal = UnitSpherePosition;
		// Probability of generating this surface position is 1 / SurfaceArea
		Sample.PDF = 1.0f / (4.0f * (float)PI * LightSourceRadius * LightSourceRadius);
	}
	else
	{
		float CylinderSurfaceArea = 2 * (float)PI * LightSourceRadius * LightSourceLength;
		float SphereSurfaceArea = 4.0f * (float)PI * LightSourceRadius * LightSourceRadius;
		float TotalSurfaceArea = CylinderSurfaceArea + SphereSurfaceArea;

		// MPalko: Added support for cylinder lights

		// Cylinder End caps
		// The chance of calculating a point on the end sphere is equal to it's percentage of total surface area
		if (RandomStream.GetFraction() < SphereSurfaceArea / TotalSurfaceArea)
		{
			// Generate a sample on the surface of the sphere with uniform density over the surface area of the sphere
			//@todo - stratify
			const FVector4 UnitSpherePosition = GetUnitVector(RandomStream);
			Sample.Position = UnitSpherePosition * LightSourceRadius + Position;

			if (Dot3(UnitSpherePosition, Direction) > 0)
				Sample.Position += Direction * (LightSourceLength / 2);
			else
				Sample.Position += -Direction * (LightSourceLength / 2);

			Sample.Normal = UnitSpherePosition;
		}
		// Cylinder body
		else
		{
			// Get point along centre line
			FVector4 CentreLinePosition = Position + Direction * LightSourceLength * (RandomStream.GetFraction() - 0.5f);
			// Get point radius away from centre line at random angle
			float Theta = 2.0f * (float)PI * RandomStream.GetFraction();
			FVector4 CylEdgePos = FVector4(0, FMath::Cos(Theta), FMath::Sin(Theta), 1);
			// Rotate our edge pos to match the light's angle
			CylEdgePos = Direction.Rotation().RotateVector(CylEdgePos);

			Sample.Position = CylEdgePos * LightSourceRadius + CentreLinePosition;
			Sample.Normal = CylEdgePos;
		}

		// Probability of generating this surface position is 1 / SurfaceArea
		Sample.PDF = 1.0f / TotalSurfaceArea;
	}
}

Also, add the following function, also in LightmassScene.cpp ( I put it right above FPointLight::GetDirectIntensity, since that’s where it’s used)


/** Finds the closest point from a world position along a "long" capsule light */
FVector4 FPointLight::GetClosetPointToCapsuleLight(const FVector4& WorldPoint) const
{
	// Find the closest point along the centreline of our capsule
	const FVector4 PointOnLine = (Direction * Dot3(WorldPoint - Position, Direction)) + Position;
	// Clamp to length of our light source
	const FVector4 End1 = Position + Direction * (LightSourceLength / 2);
	const FVector4 End2 = Position - Direction * (LightSourceLength / 2);
	// Return clamped vector.
	return FVector4(FMath::Clamp(PointOnLine.X, FMath::Min(End1.X, End2.X), FMath::Max(End1.X, End2.X)),
					FMath::Clamp(PointOnLine.Y, FMath::Min(End1.Y, End2.Y), FMath::Max(End1.Y, End2.Y)),
					FMath::Clamp(PointOnLine.Z, FMath::Min(End1.Z, End2.Z), FMath::Max(End1.Z, End2.Z)));
}

And finally, in LightmassScene.h, add the new function.


	/** Finds the closest point from a world position along a "long" capsule light */
	virtual FVector4 GetClosetPointToCapsuleLight(const FVector4& WorldPoint) const;

Now, keep in mind this will only work if you use inverse square falloff, and it’s not implemented for spot lights, only point lights. Also, make sure you also rebuild lightmass, not just UE4, since they’re separate executables.

Anyway, enjoy!

Thank you! You just saved me from a tedious amount of work.

This is great! Lot of community members will love this. Thank you very much Palko. :slight_smile:

Its implemented in 4.4. So Can I utilize this in engine? Or do I still have to code it?

You should be able to use it as is in engine.

I still haven’t found how to enable it, can you point out how exactly to use it?

I don’t come here regularly these days, so it took a while for me to see this.

As for the usage, just put a point light in the level, and in the properties set the SourceRadius and SourceLength to whatever you want. For instance using 3.0 and 150.0 respectively will give you a light appropriate for the old florescent lights, the long ones.