I'm a doctor, not a mechanic

Legacy:Bonehed316/ShadowSource

From Unreal Wiki, The Unreal Engine Documentation Site
Jump to: navigation, search

As many others have done, I have created some form of shadows using projectors. There are other methods, including SquirrelZero's RealtimeShadows, which are good indeed, but the workload is per pawn, and performed each tick, and therefor is, and will always be, very slow.

Next there is a method, which is very similar to my own, called Sourced_Player_Shadows. There are also some others, but I cant find the links right off hand, and I'm sure there are some which I've never thought of myself. I am not familiar with other methods, so I will not comment on them, nor compare directly with any of them.

The method I worked on, which is by no means a superb solution, is to use an actor I call a ShadowSource to handle attaching and detaching shadow projectors, and also act as a "source" of the shadow itself. It uses the Touch() and UnTouch() functions to spawn and destroy shadow projectors. The projectors use a reference to the ShadowSource as a location from which the shadow should be cast. A level designer would place these ShadowSources wherever a static mesh that represents a light source exists, and the code will take care of the rest.

Once the idea is down, the code here is trivial:

class ShadowSource extends Actor
	placeable;
 
var class<ShadowBitmapMaterial> TextureDetailClass< SEMI >
var array<DynamicShadow> Shadows;
 
var() enum EShadowDetail        // shadow detail level
{
	SD_Low,                 // 64x64
	SD_Medium,              // 128x128
	SD_High,                // 256x256
	SD_Highest,             // 512x512
} ShadowDetail;
 
var() DynamicShadow.EShadowEffect ShadowEffect;		// the type of light effect for this shadow
var() int MaxFOV;					// adjusts rotation to compensate for FOV
var() float FOVScale;					// scales FOV to change the size of the shadow
var() int MaxTraceDistance;				// how far the projector can draw
var(ShadowSourceEffect) float PhaseScale;		// used to control light effects, result may vary depending on the effect used
 
// fire effect specific variables
var(ShadowSourceEffect) int FireFlickerMagnitude;	// how far the fire flicker is allowed to move
var(ShadowSourceEffect) int FireFlickerSpeed;		// how fast the shadow moves
 
// Initialize anything
event PostBeginPlay()
{
	GetShadowTexture();
}
 
// sets the shadow texture depending on the selected detail setting
// each class is simply a subclass of ShadowBitmapMaterial with a different USize and VSize
function GetShadowTexture()
{
	Switch(ShadowDetail)
	{
		case SD_Low: TextureDetailClass = Class'ShadowDetailLow'; break;
		case SD_Medium: TextureDetailClass = Class'ShadowDetailMedium';break;
		case SD_High:   TextureDetailClass = Class'ShadowDetailHigh';break;
		case SD_Highest: TextureDetailClass = Class'ShadowDetailHighest'; break;
		default: TextureDetailClass = Class'ShadowDetailLow'; break;
	}
}
 
function DynamicShadow SpawnShadow(Pawn P)
{
	local DynamicShadow DynamicShadow;
 
	DynamicShadow = Spawn(class'DynamicShadow',self,,P.Location);
	if (DynamicShadow != None)
	{
		DynamicShadow.ShadowActor = P;
		DynamicShadow.Instigator = P;
		DynamicShadow.ShadowEffect = ShadowEffect;
		DynamicShadow.FireFlickerMagnitude = FireFlickerMagnitude;
		DynamicShadow.FireFlickerSpeed = FireFlickerSpeed;
		DynamicShadow.PhaseScale = PhaseScale;
		DynamicShadow.bBlobShadow = False;
		DynamicShadow.FOVScale = FOVScale;
		DynamicShadow.MaxFOV = MaxFOV;
		DynamicShadow.MaxTraceDistance = MaxTraceDistance;
		DynamicShadow.ShadowTexture = ShadowBitmapMaterial( Level.ObjectPool.AllocateObject(TextureDetailClass) );
		DynamicShadow.InitShadow();
	}
	return DynamicShadow;
}
 
// pawns are given a shadow when they touch
event Touch(Actor Other)
{
	if (Pawn(Other) != None)
		Shadows[Shadows.Length] = SpawnShadow(Pawn(Other));
}
 
// pawns are removed from the shadow list when they untouch, and the shadow is destroyed
event UnTouch(Actor Other)
{
	local int i;
 
	if (Pawn(Other) != None)
	{
		i = GetShadowIndex(Other);
		if (i > -1)
		{
			Shadows[i].Destroy();
			Shadows.Remove(i,1);
		}
	}
}
 
// returns the index of an actor within the shadow array
function int GetShadowIndex(Actor A)
{
	local int i;
 
	for (i=0;i<Shadows.Length;++i)
	{
		if (Shadows[i].ShadowActor == A)
			return i;
	}
	return -1;
}
 
defaultproperties
{
	bHidden=True
	ShadowDetail=SD_Medium
	ShadowEffect=SE_Normal
 
	FireFlickerMagnitude=60
	FireFlickerSpeed=20
	PhaseScale=0.5
	MaxFOV=70
	FOVScale=0.9
	MaxTraceDistance=1000
 
	bCollideActors=True
	bUseCylinderCollision=True
	CollisionHeight=500
	CollisionRadius=500
}

The key here is that the CollisionHeight and CollisionRadius are what effect how close to the source you have to be to recieve a shadow from it. Adjusting this radius is important to the concept. Basic projector properties can be controlled with the exposed variables, and more can be added easily, and subclasses of ShadowSource can be created which have even more functionality, if that is the method you prefer.

As you can see, the real magic is all in the projector class. The beauty is that the projector draws itself, from the location of the ShadowSource that owns it. This way, the overhead of sorting lights and shadow projectors is eliminated. The projector simply draws until it is destroyed.

The projector is listed here:

class DynamicShadow extends ShadowProjector;
 
// various shadow effects can exist here
var enum EShadowEffect
{
	SE_Normal,
	SE_Fire,
} ShadowEffect;
 
// these are set by the shadow source
var int FireFlickerMagnitude;
var int FireFlickerSpeed;
var int MaxFOV;
var float PhaseScale, FOVScale;
 
//hack for interpolating crouch distance over time when pawn stands up again
var float crouchtime;
var vector CurrentFirePos, TargetFirePos;
var array<vector> FirePositions;
 
// This must be called by the ShadowSource to initialize the projector to start drawing
function InitShadow()
{
	if (ShadowActor != None)
	{
		ShadowTexture.bBlobShadow = bBlobShadow;
		ShadowTexture.ShadowActor = ShadowActor;
		ProjTexture = ShadowTexture;
 
		if (ProjTexture != None)
		{
			// UpdateShadows is time driven, to allow shadow animating
			UpdateShadows(0.03);  // start the shadow rendering, and enable tick
			Enable('Tick');
		}
		// timer is used for fire effect, but could be used for others as well, so 
		// we start it up here as well
		SetTimer(FRand(),true);
	}
}
 
event Destroyed()
{
	if (ShadowTexture != None)
	{
		ShadowTexture.ShadowActor = None;
 
		if (!ShadowTexture.Invalid)
			Level.ObjectPool.FreeObject(ShadowTexture);
 
		ShadowTexture = None;
		ProjTexture = None;
	}
 
	Super.Destroyed();
}
 
event Tick(float dt)
{
	UpdateShadows(dt);
}
 
function Timer()
{
	if(ShadowEffect == SE_Fire)
	{
		// random target positions for fire flicker, and random timer rate
		TargetFirePos = FirePositions[Rand(FirePositions.Length)] * Rand(FireFlickerMagnitude);
		TimerRate = FRand() * PhaseScale;
	}
	else
		bTimerLoop=False;       // disable timer loop for non-effect shadows
 
}
 
// we use our own update shadow function
function UpdateShadow();
 
// updates location, rotation, and FOV
function UpdateShadows(float dt)
{
	local vector Diff, ShadowLocation, RandVec, instloc;
	local rotator AdjustedRotation;
	local Plane BoundingSphere;
 
	DetachProjector(True);
 
	// check to make sure we should draw, maybe this should also destroy the projector
	if (Instigator.bHidden || !bShadowActive || ShadowTexture == None || Instigator == None)
		return;
 
	instloc = Instigator.Location;
 
	// hack for fixing shadow detaching from pawn when crouching
	// crouchheight based on anim time, values may vary
	if (Instigator.bIsCrouched)
		crouchtime = FMin(crouchtime+dt*5,0.5);
	else if (crouchtime > 0.0)
		crouchtime -= dt*2;
 
	instloc.Z -= (Instigator.CrouchHeight*crouchtime);
 
	if (ShadowTexture.Invalid)
	{
		Destroy();
		return;
	}
 
	if(ShadowEffect == SE_Fire)
	{
		// move a bit closer to the target position
		RandVec = CurrentFirePos + (Normal(TargetFirePos - CurrentFirePos) * (FireFlickerSpeed * dt));
		// if we have moved too far out of range, begin moving back to the shadow source
		if( VSize(CurrentFirePos) >  FireFlickerMagnitude)
		{
			TargetFirePos = FirePositions[0];       // reset back to 0,0,0
			RandVec = CurrentFirePos + (Normal(TargetFirePos - CurrentFirePos) * (FireFlickerSpeed * dt));
		}
		// if we have already reached our target position, pick a new one
		else if( VSize(TargetFirePos - RandVec) > VSize(TargetFirePos - CurrentFirePos) )
		{
			TargetFirePos = FirePositions[Rand(FirePositions.Length)] * Rand(FireFlickerMagnitude);
			RandVec = CurrentFirePos + (Normal(TargetFirePos - CurrentFirePos) * (FireFlickerSpeed * dt));
		}
 
		CurrentFirePos = RandVec;
	}
 
	Diff = ( Owner.Location + RandVec ) - instloc;
	LightDistance = VSize(Diff);
	ShadowLocation = instloc + (4 * Normal(Diff));
	if (VSize(ShadowLocation-instloc) > LightDistance)
		ShadowLocation = instloc;
	SetLocation(ShadowLocation);
 
	BoundingSphere = Instigator.GetRenderBoundingSphere();
	FOV = (Atan(BoundingSphere.W*2 + 160, LightDistance) * 180/PI)*FOVScale;
	SetDrawScale( LightDistance * tan(0.5 * FOV * PI / 180) / (0.5 * ShadowTexture.USize) );
 
	// adjust rotation to fix the FOV causing the shadow to detatch from the player
	AdjustedRotation = rotator(instloc-(Owner.Location + RandVec));
	AdjustedRotation.Pitch -= 1408*(FOV/MaxFOV);
	SetRotation(AdjustedRotation);
 
	LightDirection = -vector(AdjustedRotation);
 
	ShadowTexture.LightDirection = LightDirection;
	ShadowTexture.LightDistance = LightDistance;
	ShadowTexture.LightFOV = FOV;
 
	// cull more if we havent rendered this pawn in 5 seconds
	if (Level.TimeSeconds - Instigator.LastRenderTime > 5)
		CullDistance = 0.25*Default.CullDistance;
	else
		CullDistance = Default.CullDistance;
 
	// cull shadows much earlier if below min framerate
	// ..this may or may not be desired, it does get rather ugly
	if (Level.bDropDetail)
		ShadowTexture.CullDistance = 0.5*CullDistance;
	else
		ShadowTexture.CullDistance = CullDistance;
	ShadowTexture.Dirty = true;
 
	AttachProjector();
}
 
defaultproperties
{
	bGradient=True
	CullDistance=1800.f
 
	// directional offsets for target pos movements
	// 1. origin (will move back to the shadow source)
	FirePositions(0)=(X=0,Y=0,Z=0)
	// 2. ordinal directions (will move along one axis)
	FirePositions(1)=(X=1,Y=0,Z=0)
	FirePositions(2)=(X=-1,Y=0,Z=0)
	FirePositions(3)=(X=0,Y=1,Z=0)
	FirePositions(4)=(X=0,Y=-1,Z=0)
	FirePositions(5)=(X=0,Y=0,Z=1)
	FirePositions(6)=(X=0,Y=0,Z=-1)
	// 3. diagonal on axis directions (will move on two axes)
	FirePositions(7)=(X=0,Y=1,Z=1)
	FirePositions(8)=(X=1,Y=0,Z=1)
	FirePositions(9)=(X=0,Y=-1,Z=1)
	FirePositions(10)=(X=-1,Y=0,Z=1)
	FirePositions(11)=(X=-1,Y=0,Z=-1)
	FirePositions(12)=(X=0,Y=-1,Z=-1)
	FirePositions(13)=(X=1,Y=0,Z=-1)
	FirePositions(14)=(X=0,Y=-1,Z=-1)
	// 4. diagonal mid axis directions (will move on three axes)
	FirePositions(15)=(X=1,Y=1,Z=-1)
	FirePositions(16)=(X=1,Y=-1,Z=-1)
	FirePositions(17)=(X=-1,Y=-1,Z=-1)
	FirePositions(18)=(X=-1,Y=1,Z=-1)
	FirePositions(19)=(X=-1,Y=1,Z=1)
	FirePositions(20)=(X=1,Y=1,Z=1)
	FirePositions(21)=(X=1,Y=-1,Z=1)
	FirePositions(22)=(X=-1,Y=-1,Z=1)
}

Issues to consider[edit]

First, the code is largely untested in practical play, but does work. Issues to consider, which have not been specifically tested include:

  • What happens if a pawn is spawned inside the CollisionRadius of a ShadowSource?
    • If Touch() is called correctly, then everything works properly, but if not...
  • What happens if a pawn is killed inside the CollisionRadius of a ShadowSource?
    • Only if UnTouch() is called when the pawn dies will the shadow be destroyed
  • What about other light effects?

Comments[edit]

Blueneptune: It appears that the shadow projectors will clip off pieces of the shadow that are out of their FOV angle. This happens often when near a light since the projector is always so close to the shadowed actor. This solution is bad for the shadow texture sharpness, but I found that setting the projector to the light's location works much better for shadow clipping (unless the mesh being shadowed encapsulates the light). As said, it's bad for the shadow sharpness and it often needs adjustments to the rotation since it's set too low.

Bonehed316: A compromise solution would be to set the projector location somewhere between the source and the pawn. Also I need to put some pics up, and get a working demo, I think. Maybe one day.

MythOpus: To solve the problems of a pawn dying inside the actors collision zone, when spawning the projectors, set the owner of the projector to that of the pawn it is being created on. Then in the projector class, have it destroy itself when it's owner = none.

Foxpaw: Untouch() will not get called when a pawn is destroyed, but I believe it will be called when a pawn actor is destroyed (and removed from the world). So the pawn would continue to have the projector acting on it while it was ragdolling.

Mythopus' solution is good but instead of using Owner I would use a variable declared in the projector and use that to store the pawn. I am wary of using Owner because Owner has some special implications when it comes to replication that I'd generally prefer not to worry about. Since the projector would be client-side, I don't THINK that you would have problems with this, but it might not hurt to store it in an internal variable instead of Owner.

Bonehed316: It uses the owning pawn as its Instigator as well, so that could be checked, since it's already used. I think the Owner should be the ShadowSource (logically this is accurate, but I'm not sure about the replication implications). As far as I know though, the shadow is destroyed when the Pawn dies, but the ShadowSource's list would not be updated, which must also be solved . This could probably be solved by calling a new function on the ShadowSource from the Projector's Destroyed() function which would remove itself from the ShadowSource's array iff Instigator == None (which, correct me if I'm wrong, means the Instigator was killed).

Bonehed316: Also the ShadowSource needs to destroy any projectors that exist if/when it is destroyed. The garbage collector might still get them, but I think this is important anyway.