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!