| Home Page | Recent Changes

SquirrelZero/RealtimeShadows

A completely automatic code based system for creating realtime player and vehicle shadows. Also completely free for public use, see the disclaimer at the bottom.

[video clip #1 (small)]

[video clip #2 (large)]

Features

  • True realtime directional shadows using all standard map lights for sourcing
  • Completely compatible with all existing maps and mods, very easy to plug in
  • Multiple shadow detail settings, built with performance in mind
  • Dynamic Light support, which allows for things like muzzle flash and explosion shadows
  • Sunlight actor support as a fallback light
  • Shadows will still be rendered (but culled very early) even if the playerpawn owning the shadow is not. Made primarily so that shadows can be seen before people, a stealth advantage that is key to the mod I code for, [Frag.Ops].
  • Unlike the UT2004 ShadowProjector these will project onto dynamic actors like vehicles and other playerpawns

Known Issues

  • This unfortunately will not work under OpenGL implementations of UT2004, like linux and macintosh. I have heard that [icculus] is working on a possible macintosh fix for this, but i'm not sure.
  • Shadows may extend beyond the borders of the shadow projector's frustrum, appearing cut off. I've attempted to minimize the visibility of this by making highly stretched shadows less dark. (Deprecated. Fixed as of r1.2)

Todo

  • Blend between projector rotations when encountering a new lightsource. Will be really nice when combined with the current fade in/out effect.
  • Find a better way to check for sunlight visibility when sunlight actor is in a SkyZone.

Revisions

v1.2

  • Both Effect_ShadowController and Effect_ShadowProjector updated
  • Added UpdateFrequency float, lower settings mean faster updates but consume more CPU. Do not go lower than 0.03.
  • Fastest code revision yet
  • Improvements on light selection and fading
  • Projector pitch limiter added, eliminates shadow breakage

v1.1

  • Updated both Effect_ShadowController and Effect_ShadowProjector with moderate changes
  • Wrote in a system to fade between light sources, no more shadow "popping"
  • Fixed an accessed none when no sunlight is present
  • Improved blending between light sources
  • Better, darker shadows
  • Sunlit shadows will now be given a static darkness
  • Added an extra sunlight check to avoid sunlight creating shadows indoors, however this will not work with sunlight actors placed in skyboxes. Sunlight will still be used as a fallback light though if no other lights are present, even if in a skybox. Anyone know how to figure out what zone an actor is in? Would help with this bit of the system.

v1.0

  • Initial Release

Summary

I started playing with realtime directional shadows about 6 months ago and realized it would be very difficult to do what I wanted without projecting onto the playerpawn owning the shadow. UT2004 then added the bNoProjectOnOwner variable which made me decide to revisit and finish this small side project. What it does is allow you to specify a number of shadow "slots" to allocate and create shadows for each new light source encountered within a spherical radius, specified by the MaxLightDistance variable. To do this we spawn a new custom shadow projector for each light in the Lights array, which is filled by assessing the LightPriority of each light found in the radius. Each shadow projector then works independantly to create a very nice-looking directional shadow, and when combined by setting MaxShadows to 2 or higher, creates some very real and atmospheric shadow webbing.

Requirements

You'll need to have access to a method of spawning an actor clientside on all network clients, like copying and modifying the PostBeginPlay() function in xPawn which is already used to spawn player shadows. Mine looks like this:

class FOPawn extends xPawn;

var Effect_ShadowController RealtimeShadow;
var bool bRealtimeShadows;

simulated function PostBeginPlay()
{
    Super(UnrealPawn).PostBeginPlay();
    AssignInitialPose();

    if (bActorShadows && bPlayerShadows && (Level.NetMode != NM_DedicatedServer))
    {
        // decide which type of shadow to spawn
        if (!bRealtimeShadows)
        {
            PlayerShadow = Spawn(class'ShadowProjector',Self,'',Location);
            PlayerShadow.ShadowActor = self;
            PlayerShadow.bBlobShadow = bBlobShadow;
            PlayerShadow.LightDirection = Normal(vect(1,1,3));
            PlayerShadow.LightDistance = 320;
            PlayerShadow.MaxTraceDistance = 350;
            PlayerShadow.InitShadow();
        }
        else
        {
            RealtimeShadow = Spawn(class'Effect_ShadowController',self,'',Location);
            RealtimeShadow.Instigator = self;
            RealtimeShadow.Initialize();
        }
    }
    bCanDoubleJump = false;
}

defaultproperties
{
    bRealtimeShadows=true
}

This is only a snippet to explain how my shadow controller is spawned, you must also destroy your shadow controller appropriately when the pawn is destroyed, hide it when in vehicles, etc. Follow the methods xPawn uses to control its PlayerShadow actor as an example.

Note: when coming out of an idle state (setting bShadowActive=true again) you must run FillLights() once to retrigger the timer loop. Thanks to Byte from [Shattered Oasis] for pointing this out.

Effect_ShadowController.uc

Instead of ticking the shadow projectors individually, we only need to tick this actor and update all shadows at once. bShadowActive acts a master switch to toggle shadows on/off. Everything else is otherwise commented in the script itself.

//=============================================================================
// © 2004 Matt 'SquirrelZero' Farber
//=============================================================================
// This is a master class for controlling all shadows.  This is needed to spawn
// and determine the visibility of all shadows.  It also keeps track of all the
// nearby lights.
//=============================================================================
class Effect_ShadowController extends Actor
    config(User);

// the type of shadow projector we're going to use
var class<Effect_ShadowProjector> ShadowClass;

// an array of shadows, the maximum allowed per player can be specified below
var array<Effect_ShadowProjector> Shadows;

// an array of static lights
var array<Light> LightList;

// a special variable type, lastlight is used for fading
struct LightGroup
{
    var Actor CurrentLight;
    var Actor LastLight;
};

// an array of lightgroups, filled when spawned
var array<LightGroup> Lights;

// the maximum distance a light can be from a player to cast a shadow
var float MaxLightDistance;

// update frequency for filling light array
var float UpdateFrequency;

// the sun
var SunLight SunLightActor;

// turns on/off all shadows
var bool bShadowActive;

// maximum allowed shadows per player, configurable
var globalconfig int MaxShadows;

// how crisp (detailed) the shadows should be.  The higher, the better
// they look, but the worse they perform.
// Low = 128x128
// Medium = 256
// High = 512
// Maximum = 1024 <- can any modern video technology even run it at maximum?
var globalconfig enum CrispnessEnum
{
    Low,
    Medium,
    High,
    Maximum
}ShadowCrispness;

function Initialize()
{
    local Light LightActor;
    local array<SunLight> SunLights;
    local int i;

    // set the sunlight, pick the brightest one if we have multiple
    foreach AllActors(class'Light',LightActor)
    {
        if (SunLight(LightActor) != None)
        {
            SunLights[i] = SunLight(LightActor);
            i++;
        }
        else if (LightActor.LightType != LT_None && LightActor.LightBrightness > 1)
            LightList[LightList.Length] = LightActor;
    }
    for (i=0;i<SunLights.Length;i++)
    {
        if (SunLightActor == None || SunLightActor.LightBrightness < SunLights[i].LightBrightness)
            SunLightActor = SunLights[i];
    }

    // fill the arrays with placeholders
    Shadows.Insert(0,MaxShadows);
    Lights.Insert(0,MaxShadows);

    // enable
    bShadowActive = true;

    // build light array
    FillLights();
}

function Timer()
{
    FillLights();
}

function FillLights()
{
    local int i, j;
    local actor LightActor;
    local array<Actor> OrigLight;
    local float Diff;

    // clear array of lights, leave LastLight alone to fade
    for (i=0;i<MaxShadows;i++)
    {
        OrigLight[i] = Lights[i].CurrentLight;
        Lights[i].CurrentLight = None;
    }

    // set the location of the controller, for light detection purposes
    SetLocation(Owner.Location);

    // shadow is off
    if (!bShadowActive)
        return;

    // build the array of lights, we prioritize by both brightness and distance
    for (i=0;i<LightList.Length;i++)
    {
        Diff = VSize(LightList[i].Location-Owner.Location);
        if ((LightList[i].bStatic || LightList[i].bDynamicLight) && Diff < MaxLightDistance && LightList[i].LightRadius >= (Diff*0.041) && Diff > (Owner.CollisionRadius*0.5) && IsVisible(LightList[i].Location)) // (0.041 is not perfect)
        {
            if (Lights[0].CurrentLight != None && LightPriority(Lights[0].CurrentLight.LightBrightness,Lights[0].CurrentLight.LightRadius,VSize(Lights[0].CurrentLight.Location-Owner.Location)) > LightPriority(LightList[i].LightBrightness,LightList[i].LightRadius,Diff))
                continue;
            
            // this puts them in order of priority
            for (j=1;j<MaxShadows;j++)
                Lights[j].CurrentLight = Lights[j-1].CurrentLight;

            Lights[0].CurrentLight = LightList[i];
        }
    }
    foreach DynamicActors(class'Actor',LightActor)
    {
        Diff = VSize(LightActor.Location-Owner.Location);
        // if we are within the light's radius, and it's shining on at least one part of us, then add it to the array
        if (Diff < MaxLightDistance && !LightActor.bUnlit && LightActor.LightType != LT_None && Projectile(LightActor) == None && Attachment_Base(LightActor) == None && Effect_TacLightGlow(LightActor) == None && (LightActor.bStatic || LightActor.bDynamicLight) && LightActor.LightEffect != LE_Sunlight && IsVisible(LightActor.Location) && LightActor.LightBrightness > 1 && LightActor.LightRadius >= (Diff*0.041) && Diff > (Owner.CollisionRadius*0.5)) // (0.041 is not perfect)
        {
            if (Lights[0].CurrentLight != None && LightPriority(Lights[0].CurrentLight.LightBrightness,Lights[0].CurrentLight.LightRadius,VSize(Lights[0].CurrentLight.Location-Owner.Location)) > LightPriority(LightActor.LightBrightness,LightActor.LightRadius,Diff))
                continue;

            // this puts them in order of priority
            for (i=1;i<MaxShadows;i++)
                Lights[i].CurrentLight = Lights[i-1].CurrentLight;

            Lights[0].CurrentLight = LightActor;
        }
    }
    
    // we'll use the sunlight as the very lowest priority of lights
    for (i=0;i<MaxShadows;i++)
        if (Lights[i].CurrentLight == None && SunlightActor != None && (IsVisible(SunLightActor.Location) || Lights[0].CurrentLight == None))
        {
            Lights[i].CurrentLight = SunLightActor;
            break;
        }


    // set up last light for fading
    for (i=0;i<MaxShadows;i++)      
        if (OrigLight[i] != Lights[i].CurrentLight)
        {
            if (i > 0)
            {
                if (OrigLight[i] == Lights[i-1].CurrentLight || OrigLight[i] == OrigLight[i-1])
                    OrigLight[i] = None;
            }
            Lights[i].LastLight = OrigLight[i];
        }

    // loop it, loop it good
    SetTimer(UpdateFrequency,false);
}

// simple function for determining the priority of a light
function float LightPriority(float Brightness, float Radius, float Distance)
{
    local float Priority;

    // brightness takes higher priority
    Priority = (Brightness*10)/Distance;

    // lights that are very close get higher priority
    if (Distance < 0.1 * MaxLightDistance)
        Priority *= 1.5;

    // lights with very small radii shouldn't really cast shadows
    if (Radius < 2.0)
        Priority *= (Radius*0.38);
    
    return Priority;
}

function bool IsVisible(vector Loc)
{
    local vector FootLocation;
    
    // get a location near the feet
    FootLocation = Owner.Location;
    FootLocation.Z -= Owner.CollisionHeight*0.49;

    // not very clean, returns true if either the head, feet, or middle torso of the player is visible to Loc
    if (FastTrace(Loc,Owner.Location) || FastTrace(Loc,Owner.GetBoneCoords('head').Origin) || FastTrace(Loc,FootLocation))
        return true;
}

function Tick(float dt)
{
    // fallback
    if (Owner == None)
        return;
    
    // update all shadows
    UpdateShadows(dt);
}

function UpdateShadows(float dt)
{
    local int i;

    for (i=0;i<Lights.Length;i++)
    {
        // disable the shadow attached to this slot if light no longer active, or if manually made inactive
        if ((Lights[i].CurrentLight == None && Lights[i].LastLight == None) || !bShadowActive)
        {
            if (Shadows[i] != None)
                Shadows[i].DisableShadow();
            continue;
        }

        // spawn a new shadow if it doesn't already exist
        if (Shadows[i] == None)
            Shadows[i] = SpawnShadow(rotator(Lights[i].CurrentLight.Location-Location));

        // update each shadow
        Shadows[i].UpdateShadow(dt,i,self);
    }
}

function Effect_ShadowProjector SpawnShadow(rotator LightRotation)
{
    local Effect_ShadowProjector NewShadow;

    // spawn and initialize a shadow projector
    NewShadow = spawn(ShadowClass,Owner,,Location,LightRotation);
    NewShadow.Disable('Tick');
    NewShadow.InitializeFor(self);

    return NewShadow;
}

function Destroyed()
{
    local int i;

    Disable('Tick');

    // destroy all shadows
    for (i=0;i<MaxShadows;i++)
        if (Shadows[i] != None)
            Shadows[i].Destroy();
}

defaultproperties
{
    // internal
    UpdateFrequency=0.1
    MaxShadows=2
    MaxLightDistance=1300.0
    ShadowCrispness=Low
    ShadowClass=class'Effect_ShadowProjector'

    // settings
    bNoDelete=false
    bStatic=false
    bHidden=true
    DrawType=DT_None
    RemoteRole=ROLE_None
    Physics=PHYS_None
}

Effect_ShadowProjector.uc

The actual shadow projector, much better than the old UT200x shadow projector.

//=============================================================================
// © 2004 Matt 'SquirrelZero' Farber
//=============================================================================
// Similar to ShadowProjector, just a lot better.  This shadow projector, while
// always in the same relative location to the player model, will dynamically
// adjust its FOV and also feed the ShadowBitmapMaterial new lighting data on
// the fly.  It uses a more exaggerated FOV than the standard UT200x shadows,
// because it looks so much cooler :).  It also uses the ShadowCrispness 
// setting of the ShadowController to set a larger ShadowBitmapMaterial size.
// Note that the bigger the shadow material the more data is being passed to
// video each frame, so performance will worsen exponentially as you go higher.
// I had to do a lot of little tweaks and hack some settings in to get the
// projector to do what i wanted, and not cut off at strange angles.
//=============================================================================
class Effect_ShadowProjector extends Projector;

var() vector LightDirection;
var() float LightDistance, InterpolationRate, MaxFOV, FadeSpeed, DarknessScale;
var ShadowBitmapMaterial ShadowTexture;
var bool bFadeIn;

function PostBeginPlay()
{
    Super(Actor).PostBeginPlay();
}

// these don't need to tick, we update all shadows at the same time in the controller
function Tick(float dt) {}

// this turns the shadow off
function DisableShadow()
{
    // detach
    DetachProjector();

    // stop shadow texture from being reuploaded to video here  
    if (ShadowTexture != None)
    {
        ShadowTexture.Dirty = false;
        ShadowTexture.ShadowActor = None;
    }
}

// initialize
function InitializeFor(Effect_ShadowController ShadowController)
{
    if (ShadowController.Owner != None)
    {
        // set the Owner
        SetOwner(ShadowController.Owner);
        
        // allocate the shadow texture
        switch (ShadowController.ShadowCrispness)
        {
            case Medium:
                ShadowTexture = ShadowBitmapMaterial(Level.ObjectPool.AllocateObject(class'Effect_ShadowBitmapMaterialMedium'));
                break;
        
            case High:
                ShadowTexture = ShadowBitmapMaterial(Level.ObjectPool.AllocateObject(class'Effect_ShadowBitmapMaterialHigh'));
                break;
            
            case Maximum:
                ShadowTexture = ShadowBitmapMaterial(Level.ObjectPool.AllocateObject(class'Effect_ShadowBitmapMaterialMax'));
                break;
            
            Default:
                ShadowTexture = ShadowBitmapMaterial(Level.ObjectPool.AllocateObject(class'Effect_ShadowBitmapMaterialLow'));
                break;

        }

        // set projector texture
        ProjTexture = ShadowTexture;
        
        // initialize the shadow texture
        if (ShadowTexture != None)
        {
            ShadowTexture.Invalid = false;
            ShadowTexture.ShadowActor = Owner;
            ShadowTexture.bBlobShadow = false;
            ShadowTexture.CullDistance = CullDistance; 
        }
        else
        {
            log(Name$".InitializeFor: No shadow texture!  Escaping...");
            Destroy();
        }
    }
    else
        log(Name$".InitializeFor: No Owner!");
}

// updates this shadow's location, rotation, and FOV
function UpdateShadow(float dt, int LN, Effect_ShadowController ShadowController)
{
    local Plane BoundingSphere;
    local Actor ShadowLight;
    local vector Diff, ShadowLocation, Origin;
    local rotator LightRotation, AdjustedRotation;
    local float Interpolation;
    local bool bFadeOut;

    // detach projector
    DetachProjector(true);

    // fallback, don't draw if hidden or no shadow texture
    if (Owner == None || Owner.bHidden || ShadowTexture == None)
        return;

    // fallback and destroy
    if (ShadowTexture.Invalid)
    {
        Destroy();
        return;
    }

    // cull more if we haven't rendered this pawn in 5 seconds
    if (Level.TimeSeconds - Owner.LastRenderTime > 5)
        CullDistance = 0.5*Default.CullDistance;
    else
        CullDistance = Default.CullDistance;

    // cull shadows much earlier if below min framerate, important
    if (Level.bDropDetail)
        ShadowTexture.CullDistance = 0.6*CullDistance;
    else
        ShadowTexture.CullDistance = CullDistance;

    // in case shadow was disabled earlier
    ShadowTexture.ShadowActor = Owner;

    // set light
    ShadowLight = ShadowController.Lights[LN].CurrentLight;

    // fade out if necessary
    if (ShadowController.Lights[LN].LastLight != None)
    {
        if (ShadowTexture.ShadowDarkness > 5)
        {
            ShadowLight = ShadowController.Lights[LN].LastLight;
            bFadeOut = true;
        }
        else
        {
            ShadowController.Lights[LN].LastLight = None;
            bFadeOut = false;
            bFadeIn = true;
            DarknessScale = 0;
        }
    }

    // fallback if no more lights after fadeout
    if (ShadowLight == None)
        return;

    // get the direction of the light
    Diff = ShadowLight.Location - Owner.Location;

    // set light distance
    if (ShadowLight.LightEffect == LE_Sunlight)
        LightDistance = ShadowController.MaxLightDistance*0.3;
    else
        LightDistance = FClamp(VSize(Diff), ShadowController.MaxLightDistance*0.1, ShadowController.MaxLightDistance*0.3);
    
    // get a location along the path of the light slightly away from center of player
    ShadowLocation = Owner.Location + 4*Normal(Diff);
    if (ShadowLocation.Z < Owner.Location.Z)
        ShadowLocation.Z = Owner.Location.Z+4;

    // set location
    SetLocation(ShadowLocation + vect(0,0,-8));

    // determine correct rotation, interpolate
    Origin = ShadowLocation + ShadowTexture.LightDirection * ShadowTexture.LightDistance;
    Interpolation = FMin(1.0, (dt*InterpolationRate));
    Origin += (ShadowLight.Location - Origin) * Interpolation;  
    Diff = ShadowLocation - Origin;
    LightRotation = rotator(Diff);

    // calculate FOV
    BoundingSphere = Owner.GetRenderBoundingSphere();
    FOV = (Atan(BoundingSphere.W*2 + 160, LightDistance) * 180/PI);

    // set rotation, compensate for FOV warping -- kinda hackish, but fixes shadows that 
    // bend so much they detach from the pawn
    AdjustedRotation = rotator(Owner.Location-ShadowLight.Location);
    AdjustedRotation.Pitch = Clamp(AdjustedRotation.Pitch, -20500, -9500);
    SetRotation(AdjustedRotation);

    // determine correct direction of light
    LightDirection = -vector(AdjustedRotation)*LightDistance;
    
    // set light direction
    ShadowTexture.LightDirection = Normal(LightDirection);

    // set lightdistance
    ShadowTexture.LightDistance = LightDistance;

    // update shadow texture FOV
    ShadowTexture.LightFOV = FOV;
    
    // set the drawscale, exaggerate a bit
    SetDrawScale((LightDistance*0.82) * tan(0.5*FOV*PI/180) / (0.45*ShadowTexture.USize));

    // fade out gracefully
    if (bFadeOut)
        ShadowTexture.ShadowDarkness = Max(ShadowTexture.ShadowDarkness - (FadeSpeed*dt), 0);
    else
    {
        ShadowTexture.ShadowDarkness = 140*(1.0-(FClamp(VSize(ShadowLight.Location-Owner.Location)/ShadowController.MaxLightDistance, 0.0, 1.0))) + 70;
        if (bFadeIn && DarknessScale < 1.0)
        {
            DarknessScale = FMin(DarknessScale + ((FadeSpeed*0.007)*dt), 1.0);
            ShadowTexture.ShadowDarkness = float(ShadowTexture.ShadowDarkness)*DarknessScale;
        }
        else
            bFadeIn = false;
    }

    // dirty texture for reuploading to video
    ShadowTexture.Dirty = true;

    // reattach projector
    AttachProjector();
}

function Destroyed()
{
    // free shadow texture from the object pool
    if (ShadowTexture != None)
    {
        ShadowTexture.ShadowActor = None;
        
        if (!ShadowTexture.Invalid)
            Level.ObjectPool.FreeObject(ShadowTexture);

        // must set to none
        ShadowTexture = None;
        ProjTexture = None;
    }
    Super.Destroyed();
}

defaultproperties
{
    // this lets us have shadows that cast onto other players
    bProjectActor=true
    // ... made possible by this, so it doesn't project onto the pawn owning it
    bNoProjectOnOwner=true
    // how quickly we should interpolate
    InterpolationRate=2.5
    // this should be dynamically updated also
    MaxTraceDistance=165
    // maximum FOV
    MaxFOV=80.0
    // how quickly to fade in/out
    FadeSpeed=500.0

    bProjectOnParallelBSP=true
    bProjectOnAlpha=false
    bClipBSP=true
    bGradient=false
    bStatic=false
    bOwnerNoSee=true
    bDynamicAttach=true
    RemoteRole=ROLE_None
    CullDistance=1200.0
}

ShadowBitmapMaterial Detail Levels

Multiple detail levels; higher for crisper, more detailed shadows, lower for performance.

//=============================================================================
// © 2004 Matt 'SquirrelZero' Farber
//=============================================================================
class Effect_ShadowBitmapMaterialLow extends ShadowBitmapMaterial;

defaultproperties
{
    USize=128
    VSize=128
    UClamp=128
    VClamp=128
    UClampMode=TC_Clamp
    VClampMode=TC_Clamp
}
//=============================================================================
// © 2004 Matt 'SquirrelZero' Farber
//=============================================================================
class Effect_ShadowBitmapMaterialMedium extends Effect_ShadowBitmapMaterialLow;

defaultproperties
{
    USize=256
    VSize=256

}
//=============================================================================
// © 2004 Matt 'SquirrelZero' Farber
//=============================================================================
class Effect_ShadowBitmapMaterialHigh extends Effect_ShadowBitmapMaterialLow;

defaultproperties
{
    USize=512
    VSize=512
}
//=============================================================================
// © 2004 Matt 'SquirrelZero' Farber
//=============================================================================
class Effect_ShadowBitmapMaterialMax extends Effect_ShadowBitmapMaterialLow;

defaultproperties
{
    USize=1024
    VSize=1024
}

Notes

That's all there is to it thankfully, once spawned the controller and projectors really take care of the rest. Many people will most likely be satisfied with the default settings, although feel free to tweak and explore. Just make sure you post any positive results, improvements are always welcome!

Disclaimer

This code is free for any kind of use including commercial and non-commercial applications, just let me know what it's being used for, only because I'm always interested in new applications or fresh ideas.

Files

[Demonstration Mutator] - A UT2004 mutator written by JCBDigger to demonstrate the realtime shadow system. It is based on the rev1.1 code with a few minor changes to get it to work multiplayer. It is located under the header 'UT2004 Work In Progress', and please note that this is for demonstration purposes only!

Comments

SquirrelZero: Any changes or improvements to my code is welcome.

Foxpaw: There is already the Sourced Player Shadows page.. maybe the two should be merged or something.

SquirrelZero: That page is way too disorganized for me, and i don't have time to organize it. It's a different method anyway. All pages i add from this one on will be linked from the SquirrelZero page.

SquirrelZero: The code has been updated to revision 1.1, key new feature is fading between shadows.

Foxpaw: I noticed you were looking for a way to determine what zone an actor is in. A reference to the zone an actor is in is available as Region.Zone.

SquirrelZero: Oh, good find. I'll have to update the sunlight stuff again in a bit.

SquirrelZero: Ooookay, this page was edited 10 times yesterday and the only visible change i can see is that my preview image is now missing. Please do not remove or change anything without documenting your changes.

JCBDigger: I've used v1.1 of this code to create a demonstration mutator. You can download it from my web site: http://games.DiscoverThat.co.uk in the 'UT2004 Work In Progress' section.

SquirrelZero: Nice stuff, but it does look like it might not work in multiplayer. Still a great way to preview it though :)

JCBDigger: Multiplayer is proving more tricky than expected, I thought I would have it done by now, but no!

JCBDigger: Finally it works multiplayer.

dataangel: I've been experimenting jcb's mutator that implements squirrel's shadow code and after some fiddling I've come up with some possible optimizations. I'm not an unrealscript expert like Squirrel Zero, so this code is probably buggy . I've attached my optimized version of his mutator below. All of my tests were singleplayer on the NORMAL UT2K4 deathmatch map albatross. If you try this mutator with FragOps I have no idea how it will mix with the existing shadow system. If you must try with Fragops, you'll probably want to turn its shadow option to None, then restart the game, and only try in botmatch (but that's just a guess). This is meant as a proof of concept for shadow optimizations to be included with future versions of FragOps.

1. Option to set MaxShadows per player. Defaulting to 1 so we only see shadow for the highest priority light. Right now (again, at least in the mutator) MaxShadows is set to 2. Half as many shadows gives in my tests what seems to be less than half the performance hit. Yet a shadow was still visible most of the time – so the tactical advantage would be mostly preserved. The advantage compared to normal ut2k4 shadows comes in in that Squirrel's shadows can be longer and wider mostly I think, not that there are more of them.

2. Separate detail levels for friendly and enemy shadows, defaulting to normal shadows for friendlies and the lowest level of fancy shadow for enemies. Friendly shadows are of little use tactically. Inparticular, this would help with the laginess associated with the beginning of rounds when all of your team members are standing bunched together around you. Note that the option in the mutator to disable friendly shadows doesn't disable them entirely, it just makes friendlies have normal ut2k4 shadows.

3. Toggle for whether or not ragdolls have shadows. Currently (at least in jcb's mutator) they do. Bodies usually fly through the air from grenades and the like; times when fps is already dropping, no need to shoot a dead horse Wink

4. As far as I can tell it's unnecessary for UpdateShadows() to run every tick. I altered this so now it runs every .03 seconds, right after FillLights(). In game the shadows seemed to act the same but I got better performance (spectating two bots w/ max detail settings was actually somewhat playable speed). I'm not sure how SquirrelZero came to the .03 number, it maybe possible to go higher, I suspect he just tweaked it.

5. Toggle to make it only show a player's shadow when you don't have line of sight to them. Shadows are supposed to help tactically by letting you see that your opponent is about to come around the corner. This option makes it so that once they do come out from behind the corner, their shadow disappears. This way you only see it when it's useful, and once you get in the open firefight it stops sucking frames. I didn't do enough testing though – my mutator may not actually be implementing this properly, can someone confirm for me? In any case I bet Squirrel Zero can code it into FO if he likes the idea Wink

These optimizations/options together I think will enable lower end players to see shadows just when they're gameplay relevant, and thus letting alot more people take advantage of them.

If you want the source just export it.

Post here how well this works for you. Hope somebody benefits =)

Download:

[30]

P.S. The mutators defaults should be the most optimized. If you want to see how the mutator was before, set MaxShadows to 2, uncheck the line of sight option, check ragdoll shadows, uncheck the only render enemy shadows option. Although to truly turn it back you'll also need to change the code to get rid of #4.

Bigcheese:I have been using your code for the hackerz (www.hackerz.tk) mod and i got it to work on people. but then i tryed to use it on a new karma actor that i baced off of the exion karma. it just adds online play and some other interesting things, but i want to use dynamic showdows for it but i just dont know how to spawn them like you do in pawn???

Daemonica: You wanted to know who's using your code, it's so tasty the Undying Curse http://undying.sanffo.co.uk team are using it. Nice work SquirrelZero!

SquirrelZero: Glad to see this is getting so much use :). I've improved it further to use a rotation limiter, which effectively fixes the breakage on shadows that bend too much. It probably runs a bit better, too. I'll post this 3rd revision sometime soon, and maybe take some new videos of it in action.

DJPaul: Esc is playing about with producing a UT2004 version, and we are implementing this as I type.

DJPaul: SquirrelZero/everyone, is this meant to work from first person view? I specifically mean are you mmeant to see YOUR shadow from the first-person view of YOUR pawn? I can only see the shadows (of my pawn) when I switch to third person.

SquirrelZero: If the pawn is invisible, the shadow will be too. Try doing bOwnerNoSee=false on both the projector and the pawn. Just keep in mind that the native animation code for pawns will prevent them from playing movement animations in first person, so you get upper body anims but no lower. Some clever hacking can resolve it, though. By the way, i've added a new code revision. It's highly recommended everyone update their mods to use the new revision; it's faster and more accurate.

Blue Ion: Could you (or anyone, of course) make a mutator out of your code like JCBDigger's? You know for those of us that just want to download, download and download and don't know a thing about UnrealScript ;). The effect is impressive and plays even better with just one shadow, too bad that sometimes the shadows turns into evil black squares.

EricBlade: I just found this page, and i have a couple questions, if anyone can help me out.. First, does this work at all reasonably well in 2k3 (since it doesn't have bNoProjectOnOwner)? Second, how might it perform with say 30 pawns on screen potentially using it?

... well ,I eventually answered myself. I forgot about this page for several months, until I decided to do my own work in this area, and someone on #unrealscript pointed me to here. One thing that I've found is by setting the Projector's location to well above the Pawn's head, instead of slightly below the Pawn.Location, you get a better look, and it doesn't draw so much on itself (i don't have bProjectOnOwner or whatever) .. now to find ways to improve it's CPU usage. BTW, Sunlight is referenced by Zone, not by world. ... Further examination seems to indicate that the section commented "determine correct rotation, interpolate" doesn't actually achieve anything, as none of those variables seem to be used for any purpose.

Here's some code to simulate bNoProjectOnOwner=True for those that don't have it:

simulated event Touch(Actor Other) {
    if(Other == Owner) return;
    super.Touch(Other);
}

The Unreal Engine Documentation Site

Wiki Community

Topic Categories

Recent Changes

Offline Wiki

Unreal Engine

Console Commands

Terminology

FAQs

Help Desk

Mapping Topics

Mapping Lessons

UnrealEd Interface

UnrealScript Topics

UnrealScript Lessons

Making Mods

Class Tree

Modeling Topics

Chongqing Page

Log In