Special Effect Shadows

Special effects are easier to work with, more versatile, and more performant than dummy units. Therefore, once the Blz special effect natives arrived prior to Reforged launch, there wasn't really a reason anymore to represent objects such as missiles as dummy units — except that special effects don't have shadows. This is where this library comes in.

SpecialEffectShadows allows the creation of objects represented by special effects and still have them cast shadows. Shadows are represented by images. The functions to
manipulate shadow-casting effects are analogous to the default special effect natives, for example BlzSetSpecialEffectPosition -> SetShadowedEffectPosition.
All functions for which the corresponding native is safe to use asynchronously can also safely be used asynchronously.

Using special effects with shadows instead of dummy units has the advantage that they're easier to handle, you can adjust their yaw, pitch, and roll, and when making them transparent, the shadow will fade out as well. I have to still do some benchmarks to find out which one is more performant.

There are two versions: The standard version has its own set of functions to move the shadowed special effects. The alternate version uses Hook to also affect the shadow on a native call, if it exists. This version allows you to add shadows to effects easily without changing the functions everywhere in your code, but it makes moving special effects slightly slower.

Will make JASS/GUI versions if requested.


Lua:
if Debug then Debug.beginFile "SpecialEffectShadows" end
do
    --[[
    =============================================================================================================================================================
                                                                  Special Effect Shadows
                                                                        by Antares

                        Requires:
                        PrecomputedHeightMap (optional)     https://www.hiveworkshop.com/threads/precomputed-synchronized-terrain-height-map.353477/
                        TotalInitialization                 https://www.hiveworkshop.com/threads/total-initialization.317099/

    =============================================================================================================================================================

    Allows the creation of objects represented by special effects that cast shadows. Shadows are represented by images. The functions to manipulate shadow-casting
    effects are analogous to the default special effect natives, for example BlzSetSpecialEffectPosition -> SetShadowedEffectPosition. All functions for which the
    corresponding native is safe to use asynchronously can also safely be used asynchronously.

    The AddShadowedEffect function requires a path for the shadow texture. There are two preset constants: UNIT_SHADOW_PATH is the standard shadow that ground
    units use. FLYER_SHADOW_PATH is the shadow that flying units use. Building shadows have a weird format and cannot be used without converting them first.

    =============================================================================================================================================================
                                                                          Functions
    =============================================================================================================================================================

    AddShadowedEffect(modelPath, x, y, shadowPath, shadowWidth, shadowHeight, xOffset, yOffset) -> effect
    DestroyShadowedEffect(whichEffect)
    SetShadowedEffectPosition(whichEffect, x, y, z)
    SetShadowedEffectX(whichEffect, x)
    SetShadowedEffectY(whichEffect, y)
    SetShadowedEffectZ(whichEffect, z)
    SetShadowedEffectAlpha(whichEffect, alpha)          Fades the effect as well as the shadow.

    =============================================================================================================================================================
                                                                            Config
    =============================================================================================================================================================
    ]]

    local SUN_ZENITH_ANGLE      = 45        ---@type number
    --Determines the direction of the sun light in degrees. 90 = sun at the zenith. Does not affect shadow shape.
    local SUN_AZIMUTH_ANGLE     = 45        ---@type number
    --Determines the direction of the sun light in degrees. 0 = shadows cast in positive x-direction. Does not affect shadow shape.
    local MAXIMUM_ALPHA         = 200       ---@type integer
    --The maximum opacity of the shadow image (0-255).
    local SHADOW_ATTENUATION    = 0.05      ---@type number
    --How quickly a shadow becomes weakened as an object moves upwards. The shadow will fade completely at Z = shadowSize/SHADOW_ATTENUATION.

    --===========================================================================================================================================================

    local mt                    = {__mode = "k"}

    local shadow                = setmetatable({}, mt)                  ---@type image[]
    local currentX              = setmetatable({}, mt)                  ---@type number[]
    local currentY              = setmetatable({}, mt)                  ---@type number[]
    local currentZ              = setmetatable({}, mt)                  ---@type number[]
    local shadowOffsetX         = setmetatable({}, mt)                  ---@type number[]
    local shadowOffsetY         = setmetatable({}, mt)                  ---@type number[]
    local shadowAttenuation     = setmetatable({}, mt)                  ---@type number[]
    local currentAlpha          = setmetatable({}, mt)                  ---@type integer[]
    local currentWidth          = setmetatable({}, mt)                  ---@type number[]
    local currentHeight         = setmetatable({}, mt)                  ---@type number[]

    local zenithOffsetX         = 1/math.tan(bj_DEGTORAD*SUN_ZENITH_ANGLE)*math.cos(bj_DEGTORAD*SUN_AZIMUTH_ANGLE)
    local zenithOffsetY         = 1/math.tan(bj_DEGTORAD*SUN_ZENITH_ANGLE)*math.sin(bj_DEGTORAD*SUN_AZIMUTH_ANGLE)

    local GetLocZ               = nil                                   ---@type function
    local moveableLoc           = nil                                   ---@type location

    UNIT_SHADOW_PATH            = "ReplaceableTextures\\Shadows\\Shadow.blp"
    FLYER_SHADOW_PATH           = "ReplaceableTextures\\Shadows\\ShadowFlyer.blp"

    ---Creates a special effect and adds a shadow. shadowWidth/shadowHeight is the size at scale 1. zOffset is the z-coordinate at which the shadow-casting effect is considered on the ground. This value is used to determine the shadow position dependent on the sun's zenith angle.
    ---@param modelPath string
    ---@param x number
    ---@param y number
    ---@param shadowPath string
    ---@param shadowWidth number
    ---@param shadowHeight number
    ---@param xOffset? number
    ---@param yOffset? number
    ---@return effect
    function AddShadowedEffect(modelPath, x, y, shadowPath, shadowWidth, shadowHeight, xOffset, yOffset)
        local effect = AddSpecialEffect(modelPath, x, y)
        shadow[effect] = CreateImage(shadowPath, shadowWidth, shadowHeight, 0, x + (xOffset or 0) - 0.5*shadowWidth, y + (yOffset or 0) - 0.5*shadowHeight, 0, 0, 0, 0, 1)
        SetImageRenderAlways(shadow[effect], true)
        SetImageColor(shadow[effect], 255, 255, 255, MAXIMUM_ALPHA)
        SetImageAboveWater(shadow[effect], false, true)

        currentWidth[effect] = shadowWidth
        currentHeight[effect] = shadowHeight
        shadowOffsetX[effect] = xOffset or 0
        shadowOffsetY[effect] = yOffset or 0
        shadowAttenuation[effect] = SHADOW_ATTENUATION/math.max(shadowHeight, shadowWidth)
        currentAlpha[effect] = MAXIMUM_ALPHA

        currentX[effect], currentY[effect] = x, y
        currentZ[effect] = GetLocZ(x, y)

        return effect
    end

    ---@param whichEffect effect
    function DestroyShadowedEffect(whichEffect)
        DestroyEffect(whichEffect)
        DestroyImage(shadow[whichEffect])
    end

    ---@param whichEffect effect
    ---@param x number
    ---@param y number
    ---@param z number
    function SetShadowedEffectPosition(whichEffect, x, y, z)
        BlzSetSpecialEffectPosition(whichEffect, x, y, z)
        currentX[whichEffect], currentY[whichEffect], currentZ[whichEffect] = x, y, z

        local terrainZ = GetLocZ(x, y)
        local dz = (z - terrainZ)
        local alpha = (currentAlpha[whichEffect]*(1 - (shadowAttenuation[whichEffect]*dz))) // 1
        if alpha < 0 then
            alpha = 0
        end
        SetImageColor(shadow[whichEffect], 255, 255, 255, alpha)

        x = x + shadowOffsetX[whichEffect] + zenithOffsetX*dz
        y = y + shadowOffsetY[whichEffect] + zenithOffsetY*dz
        SetImagePosition(shadow[whichEffect], x + shadowOffsetX[whichEffect] - 0.5*currentWidth[whichEffect], y + shadowOffsetY[whichEffect] - 0.5*currentHeight[whichEffect], 0)
    end

    ---@param whichEffect effect
    ---@param x number
    function SetShadowedEffectX(whichEffect, x)
        SetShadowedEffectPosition(whichEffect, x, currentY[whichEffect], currentZ[whichEffect])
    end

    ---@param whichEffect effect
    ---@param y number
    function SetShadowedEffectY(whichEffect, y)
        SetShadowedEffectPosition(whichEffect, currentX[whichEffect], y, currentZ[whichEffect])
    end

    ---@param whichEffect effect
    ---@param z number
    function SetShadowedEffectZ(whichEffect, z)
        SetShadowedEffectPosition(whichEffect, currentX[whichEffect], currentY[whichEffect], z)
    end

    ---@param whichEffect effect
    ---@param alpha integer
    function SetShadowedEffectAlpha(whichEffect, alpha)
        local x = currentX[whichEffect]
        local y = currentY[whichEffect]
        local z = currentZ[whichEffect]
        local terrainZ = GetLocZ(x, y)
        currentAlpha[whichEffect] = alpha
        BlzSetSpecialEffectAlpha(whichEffect, alpha)
        alpha = (currentAlpha[whichEffect]*(1 - (shadowAttenuation[whichEffect]*(z - terrainZ)))) // 1
        if alpha < 0 then
            alpha = 0
        end
        SetImageColor(shadow[whichEffect], 255, 255, 255, alpha)
    end

    OnInit.final(function()
        local precomputedHeightMap = Require.optionally "PrecomputedHeightMap"
        if precomputedHeightMap then
            GetLocZ = _G.GetTerrainZ
        else
            moveableLoc = Location(0, 0)
            GetLocZ = function(x, y)
                MoveLocation(moveableLoc, x, y)
                return GetLocationZ(moveableLoc)
            end
        end
    end)

end


Lua:
if Debug then Debug.beginFile "SpecialEffectShadows" end
do
    --[[
    =============================================================================================================================================================
                                                          Special Effect Shadows (alternate version)
                                                                        by Antares

                        Requires:
                        PrecomputedHeightMap (optional)     https://www.hiveworkshop.com/threads/precomputed-synchronized-terrain-height-map.353477/
                        TotalInitialization                 https://www.hiveworkshop.com/threads/total-initialization.317099/
                        Hook                                https://www.hiveworkshop.com/threads/hook.339153/

    =============================================================================================================================================================

    Allows the creation of objects represented by special effects that cast shadows. Shadows are represented by images. This version uses Hooks to modify the
    BlzSetSpecialEffect natives to also affect the attached shadow, if it exists.
   
    To add a shadow to an effect, use AddShadowToEffect. You need to know the coordinates of the special effect for the shadow to be created at the right position.
    However, even if you don't know the location, any update of its location with BlzSetSpecialEffectPosition will update the shadow automatically.

    The AddShadowToEffect function requires a path for the shadow texture. There are two preset constants: UNIT_SHADOW_PATH is the standard shadow that ground
    units use. FLYER_SHADOW_PATH is the shadow that flying units use. Building shadows have a weird format and cannot be used without converting them first.

    =============================================================================================================================================================
                                                                          Functions
    =============================================================================================================================================================

    AddShadowToEffect(whichEffect, x, y, shadowPath, shadowWidth, shadowHeight, xOffset, yOffset)

    =============================================================================================================================================================
                                                                            Config
    =============================================================================================================================================================
    ]]

    local SUN_ZENITH_ANGLE      = 45        ---@type number
    --Determines the direction of the sun light in degrees. 90 = sun at the zenith. Does not affect shadow shape.
    local SUN_AZIMUTH_ANGLE     = 45        ---@type number
    --Determines the direction of the sun light in degrees. 0 = shadows cast in positive x-direction. Does not affect shadow shape.
    local MAXIMUM_ALPHA         = 200       ---@type integer
    --The maximum opacity of the shadow image (0-255).
    local SHADOW_ATTENUATION    = 0.05      ---@type number
    --How quickly a shadow becomes weakened as an object moves upwards. The shadow will fade completely at Z = shadowSize/SHADOW_ATTENUATION.

    --===========================================================================================================================================================

    local mt                    = {__mode = "k"}

    local shadow                = setmetatable({}, mt)                  ---@type image[]
    local currentX              = setmetatable({}, mt)                  ---@type number[]
    local currentY              = setmetatable({}, mt)                  ---@type number[]
    local currentZ              = setmetatable({}, mt)                  ---@type number[]
    local shadowOffsetX         = setmetatable({}, mt)                  ---@type number[]
    local shadowOffsetY         = setmetatable({}, mt)                  ---@type number[]
    local shadowAttenuation     = setmetatable({}, mt)                  ---@type number[]
    local currentAlpha          = setmetatable({}, mt)                  ---@type integer[]
    local currentWidth          = setmetatable({}, mt)                  ---@type number[]
    local currentHeight         = setmetatable({}, mt)                  ---@type number[]

    local zenithOffsetX         = 1/math.tan(bj_DEGTORAD*SUN_ZENITH_ANGLE)*math.cos(bj_DEGTORAD*SUN_AZIMUTH_ANGLE)
    local zenithOffsetY         = 1/math.tan(bj_DEGTORAD*SUN_ZENITH_ANGLE)*math.sin(bj_DEGTORAD*SUN_AZIMUTH_ANGLE)

    local GetLocZ               = nil                                   ---@type function
    local moveableLoc           = nil                                   ---@type location

    UNIT_SHADOW_PATH            = "ReplaceableTextures\\Shadows\\Shadow.blp"
    FLYER_SHADOW_PATH           = "ReplaceableTextures\\Shadows\\ShadowFlyer.blp"

    ---Add a shadow to an existing special effect. shadowWidth/shadowHeight is the size at scale 1. zOffset is the z-coordinate at which the shadow-casting effect is considered on the ground. This value is used to determine the shadow position dependent on the sun's zenith angle.
    ---@param whichEffect effect
    ---@param x number
    ---@param y number
    ---@param shadowPath string
    ---@param shadowWidth number
    ---@param shadowHeight number
    ---@param xOffset? number
    ---@param yOffset? number
    function AddShadowToEffect(whichEffect, x, y, shadowPath, shadowWidth, shadowHeight, xOffset, yOffset)
        shadow[whichEffect] = CreateImage(shadowPath, shadowWidth, shadowHeight, 0, x + (xOffset or 0) - 0.5*shadowWidth, y + (yOffset or 0) - 0.5*shadowHeight, 0, 0, 0, 0, 1)
        SetImageRenderAlways(shadow[whichEffect], true)
        SetImageColor(shadow[whichEffect], 255, 255, 255, MAXIMUM_ALPHA)
        SetImageAboveWater(shadow[whichEffect], false, true)

        currentWidth[whichEffect] = shadowWidth
        currentHeight[whichEffect] = shadowHeight
        shadowOffsetX[whichEffect] = xOffset or 0
        shadowOffsetY[whichEffect] = yOffset or 0
        shadowAttenuation[whichEffect] = SHADOW_ATTENUATION/math.max(shadowHeight, shadowWidth)
        currentAlpha[whichEffect] = MAXIMUM_ALPHA

        currentX[whichEffect], currentY[whichEffect] = x, y
        currentZ[whichEffect] = GetLocZ(x, y)
    end

    ---@param whichEffect effect
    function Hook:DestroyEffect(whichEffect)
        if shadow[whichEffect] then
            DestroyImage(shadow[whichEffect])
        end
        self.old(whichEffect)
    end

    ---@param whichEffect effect
    ---@param x number
    ---@param y number
    ---@param z number
    function Hook:BlzSetSpecialEffectPosition(whichEffect, x, y, z)
        self.old(whichEffect, x, y, z)
        if shadow[whichEffect] then
            currentX[whichEffect], currentY[whichEffect], currentZ[whichEffect] = x, y, z

            local terrainZ = GetLocZ(x, y)
            local dz = (z - terrainZ)
            local alpha = (currentAlpha[whichEffect]*(1 - (shadowAttenuation[whichEffect]*dz))) // 1
            if alpha < 0 then
                alpha = 0
            end
            SetImageColor(shadow[whichEffect], 255, 255, 255, alpha)

            x = x + shadowOffsetX[whichEffect] + zenithOffsetX*dz
            y = y + shadowOffsetY[whichEffect] + zenithOffsetY*dz
            SetImagePosition(shadow[whichEffect], x + shadowOffsetX[whichEffect] - 0.5*currentWidth[whichEffect], y + shadowOffsetY[whichEffect] - 0.5*currentHeight[whichEffect], 0)
        end
    end

    ---@param whichEffect effect
    ---@param x number
    function Hook:BlzSetSpecialEffectX(whichEffect, x)
        if shadow[whichEffect] then
            BlzSetSpecialEffectPosition(whichEffect, x, currentY[whichEffect], currentZ[whichEffect])
        else
            self.old(whichEffect, x)
        end
    end

    ---@param whichEffect effect
    ---@param y number
    function Hook:BlzSetSpecialEffectY(whichEffect, y)
        if shadow[whichEffect] then
            BlzSetSpecialEffectPosition(whichEffect, currentX[whichEffect], y, currentZ[whichEffect])
        else
            self.old(whichEffect, y)
        end
    end

    ---@param whichEffect effect
    ---@param z number
    function Hook:BlzSetSpecialEffectZ(whichEffect, z)
        if shadow[whichEffect] then
            BlzSetSpecialEffectPosition(whichEffect, currentX[whichEffect], currentY[whichEffect], z)
        else
            self.old(whichEffect, z)
        end
    end

    ---@param whichEffect effect
    ---@param alpha integer
    function Hook:BlzSetSpecialEffectAlpha(whichEffect, alpha)
        BlzSetSpecialEffectAlpha(whichEffect, alpha)
        if shadow[whichEffect] then
            local x = currentX[whichEffect]
            local y = currentY[whichEffect]
            local z = currentZ[whichEffect]
            local terrainZ = GetLocZ(x, y)
            currentAlpha[whichEffect] = alpha
            alpha = (currentAlpha[whichEffect]*(1 - (shadowAttenuation[whichEffect]*(z - terrainZ)))) // 1
            if alpha < 0 then
                alpha = 0
            end
            SetImageColor(shadow[whichEffect], 255, 255, 255, alpha)
        end
    end

    OnInit.final(function()
        local precomputedHeightMap = Require.optionally "PrecomputedHeightMap"
        if precomputedHeightMap then
            GetLocZ = _G.GetTerrainZ
        else
            moveableLoc = Location(0, 0)
            GetLocZ = function(x, y)
                MoveLocation(moveableLoc, x, y)
                return GetLocationZ(moveableLoc)
            end
        end
    end)
end
Contents

SpecialEffectShadows (Map)

SpecialEffectShadows (Binary)

SpecialEffectShadows (alternate) (Binary)

Reviews
Wrda
You are missing the if Debug then Debug.endFile() end line at the end of SpecialEffectShadows, SpecialEffectShadows Hook and PrecomputedHeightMap scripts. I suspect this happens on the PrecumptedHeightMap resource too. Everything else looks fine...
That's a really cool idea, but I wonder if at this point it's more efficient to just rely on Units?
It might be, I'd have to do some tests. If you're using this for missiles, it's certainly not necessary to adjust Pitch/Roll/Alpha of the shadow every step, which would improve performance. The version I'll have for my engine will do this.

But the main reason I prefer special effects is that they are more versatile (Yaw, Pitch, Roll, asynchronous positions) and are much easier to deal with.
 

Wrda

Spell Reviewer
Level 28
Joined
Nov 18, 2012
Messages
2,010
Collect Players
  • And - All (Conditions) are true
    • Conditions
      • Or - Any (Conditions) are true
        • Conditions
          • ((Picked player) controller) Equal to User
          • ((Picked player) controller) Equal to Computer
      • ((Picked player) controller) Not equal to None
      • ((Picked player) slot status) Equal to Is playing
Controller equal to none is redundant since you're already checking if it's user or computer.

  • If - Conditions
    • ((Picked player) controller) Equal to User
    • ((Picked player) controller) Not equal to Computer
    • ((Picked player) controller) Not equal to None
    • ((Picked player) slot status) Equal to Is playing
Checking if it is a user already eliminates the other controllers.
You should use your own integer loop variables instead of Integer A and B, it's less susceptible to unexpected bugs.

Useful for tournament and minigame type of maps.

Approved

Edit: I need a dose of sleep...
 
Last edited:
Complete revamp of this system. I found the correct shadow textures in the archive and figured out how to use the image natives, so the library no longer uses special effects to represent the shadows (sorry @Vinz :pcry:). This has the advantage that they don't clip on jagged terrain and look janky, but the disadvantage that they can no longer be rescaled in both dimensions easily.

There are two shadow textures you can use, the ground unit shadow and the flying unit shadow. Building shadows are in a weird format and cannot be used without converting them first. The library also allows you to use your own shadow texture if you wish.
 

Wrda

Spell Reviewer
Level 28
Joined
Nov 18, 2012
Messages
2,010
You are missing the if Debug then Debug.endFile() end line at the end of SpecialEffectShadows, SpecialEffectShadows Hook and PrecomputedHeightMap scripts. I suspect this happens on the PrecumptedHeightMap resource too.
Everything else looks fine, except for that you don't provide a example for the Hook version, although that's changing one line and adding another.

Useful resource for a more realistic effect, in order words, makes it easier to distinguish if an object (effect) is in the air or near/on the ground.

Approved
 
Top