• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!

Special Effect Shadows

This bundle is marked as pending. It has not been reviewed by a staff member yet.
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 themselves represented by special effects (credits to Vinz). The functions to
manipulate shadow-casting effects are analogous to the default special effect natives, for example BlzSetSpecialEffectPosition -> SetShadowedEffectPosition.
All functions for which the analog native is safe to use asynchronously can also safely be used asynchronously.

Using special effects as shadows has a few upsides and a few downsides.

Pros
  • Shadow can be rescaled and faded in/out.
  • Works well for small objects.
  • Much more performant and leak-free than units.
Cons
  • Limited to one shape.
  • Shadows can get clipped or seem to float in the air on jagged terrain or when the shadow sizes are large.
  • Shadows cast on steep terrain do not have the correct length.

I considered the option to create an invisible dummy unit as the source of the shadow to get around the downsides of special effect shadows, but you might as well just use a dummy unit for the original object then (albeit, you can't adjust yaw, pitch, roll for units). But shouldn't be too hard to implement.

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/
                        SpecialEffectShadow.mdl
                        flaresimple_bw.blp

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

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

    If the shadowWidth is greater than the shadowHeight, the shadow will be stretched in x-direction. If the shadowHeight is greater, the shadow will be stretched
    in 45° direction.

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

    AddShadowedEffect(modelPath, x, y, shadowWidth, shadowHeight) -> effect
    DestroyShadowedEffect(whichEffect)
    SetShadowedEffectPosition(whichEffect, x, y, z)
    SetShadowedEffectX(whichEffect, x)
    SetShadowedEffectY(whichEffect, y)
    SetShadowedEffectZ(whichEffect, z)
    SetShadowedEffectAlpha(whichEffect, alpha)
    SetShadowedEffectScale(whichEffect, scale)

    =============================================================================================================================================================
                                                                            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 SHADOW_OFFSET_X       = 0.25      ---@type number
    --Displacement of the shadow-center in x-direction as a fraction of shadow size when the object casting the shadow is directly above the ground.
    local SHADOW_OFFSET_Y       = 0.15      ---@type number
    --Displacement of the shadow-center in y-direction as a fraction of shadow size when the object casting the shadow is directly above the ground.
    local SHADOW_OFFSET_Z       = 0.1       ---@type number
    --Displacement of the shadow in z-direction as a fraction of shadow size. A higher value prevents clipping on jagged terrain.
    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 shadow = {}                       ---@type effect[]
    local currentX = {}                     ---@type number[]
    local currentY = {}                     ---@type number[]
    local currentZ = {}                     ---@type number[]
    local shadowOffsetX = {}                ---@type number[]
    local shadowOffsetY = {}                ---@type number[]
    local shadowOffsetZ = {}                ---@type number[]
    local shadowAttenuation = {}            ---@type number[]
    local currentAlpha = {}                 ---@type integer[]
    local currentWidth = {}                 ---@type number[]
    local currentHeight = {}                ---@type number[]
    local currentScale = {}                 ---@type number[]
    local heightOffset = {}                 ---@type number[]

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

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

    local atan2 = math.atan

    ---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 shadowWidth number
    ---@param shadowHeight number
    ---@param zOffset? number
    function AddShadowedEffect(modelPath, x, y, shadowWidth, shadowHeight, zOffset)
        local effect = AddSpecialEffect(modelPath, x, y)
        shadow[effect] = AddSpecialEffect("SpecialEffectShadow.mdl", x, y)
        BlzSetSpecialEffectTimeScale(shadow[effect], 0)

        if shadowHeight > shadowWidth then
            BlzSetSpecialEffectTime(shadow[effect], 1 + 0.25*(shadowHeight/shadowWidth - 1))
            BlzSetSpecialEffectScale(shadow[effect], shadowWidth/200)
            shadowOffsetY[effect] = SHADOW_OFFSET_Y*shadowWidth
            shadowOffsetX[effect] = SHADOW_OFFSET_X*shadowWidth
            shadowOffsetZ[effect] = SHADOW_OFFSET_Z*shadowHeight
            shadowAttenuation[effect] = SHADOW_ATTENUATION/shadowWidth
        else
            BlzSetSpecialEffectTime(shadow[effect],  1 - 0.25*(shadowWidth/shadowHeight - 1))
            BlzSetSpecialEffectScale(shadow[effect], shadowHeight/200)
            shadowOffsetY[effect] = SHADOW_OFFSET_Y*shadowHeight
            shadowOffsetX[effect] = SHADOW_OFFSET_X*shadowHeight
            shadowOffsetZ[effect] = SHADOW_OFFSET_Z*shadowWidth
            shadowAttenuation[effect] = SHADOW_ATTENUATION/shadowHeight
        end

        currentX[effect], currentY[effect] = x + shadowOffsetX[effect], y + shadowOffsetY[effect]
        currentZ[effect] = GetLocZ(x, y)
        BlzSetSpecialEffectPosition(shadow[effect], currentX[effect], currentY[effect], currentZ[effect])

        heightOffset[effect] = zOffset or 0
        currentAlpha[effect] = 255
        currentScale[effect] = 1
        currentWidth[effect] = shadowWidth
        currentHeight[effect] = shadowHeight
        return effect
    end

    ---@param whichEffect effect
    function DestroyShadowedEffect(whichEffect)
        DestroyEffect(whichEffect)
        DestroyEffect(shadow[whichEffect])
        shadow[whichEffect] = nil
        currentX[whichEffect], currentY[whichEffect], currentZ[whichEffect] = nil, nil, nil
        shadowOffsetX[whichEffect], shadowOffsetY[whichEffect], shadowOffsetZ[whichEffect] = nil, nil, nil
        currentAlpha[whichEffect], currentScale[whichEffect], currentWidth[whichEffect], currentHeight[whichEffect] = nil, nil, nil, nil
        heightOffset[whichEffect], shadowAttenuation[whichEffect] = nil, nil
    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
        BlzSetSpecialEffectAlpha(shadow[whichEffect], alpha)

        x = x + shadowOffsetX[whichEffect] + zenithOffsetX*(dz - heightOffset[whichEffect])
        y = y + shadowOffsetY[whichEffect] + zenithOffsetY*(dz - heightOffset[whichEffect])
        terrainZ = GetLocZ(x, y)
        BlzSetSpecialEffectPosition(shadow[whichEffect], x, y, terrainZ + shadowOffsetZ[whichEffect])

        local gradientX = (GetLocZ(x + 16, y) - GetLocZ(x - 16, y))*0.0313
        local gradientY = (GetLocZ(x, y + 16) - GetLocZ(x, y - 16))*0.0313
        BlzSetSpecialEffectPitch(shadow[whichEffect], -atan2(gradientX, 1))
        BlzSetSpecialEffectRoll(shadow[whichEffect], atan2(gradientY, 1))
    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
        BlzSetSpecialEffectAlpha(shadow[whichEffect], alpha)
    end

    ---@param whichEffect effect
    ---@param scale number
    function SetShadowedEffectScale(whichEffect, scale)
        local x = currentX[whichEffect]
        local y = currentY[whichEffect]
        local z = currentZ[whichEffect]
        local terrainZ = GetLocZ(x, y)
        BlzSetSpecialEffectScale(whichEffect, scale)

        currentWidth[whichEffect] = currentWidth[whichEffect]*scale/currentScale[whichEffect]
        currentHeight[whichEffect] = currentHeight[whichEffect]*scale/currentScale[whichEffect]
        currentScale[whichEffect] = scale

        if currentHeight[whichEffect] > currentWidth[whichEffect] then
            BlzSetSpecialEffectScale(shadow[whichEffect], currentWidth[whichEffect]/200)
            shadowOffsetY[whichEffect] = SHADOW_OFFSET_Y*currentWidth[whichEffect]
            shadowOffsetX[whichEffect] = SHADOW_OFFSET_X*currentWidth[whichEffect]
            shadowOffsetZ[whichEffect] = SHADOW_OFFSET_Z*currentHeight[whichEffect]
            shadowAttenuation[whichEffect] = SHADOW_ATTENUATION/currentWidth[whichEffect]
        else
            BlzSetSpecialEffectScale(shadow[whichEffect], currentHeight[whichEffect]/200)
            shadowOffsetY[whichEffect] = SHADOW_OFFSET_Y*currentHeight[whichEffect]
            shadowOffsetX[whichEffect] = SHADOW_OFFSET_X*currentHeight[whichEffect]
            shadowOffsetZ[whichEffect] = SHADOW_OFFSET_Z*currentWidth[whichEffect]
            shadowAttenuation[whichEffect] = SHADOW_ATTENUATION/currentHeight[whichEffect]
        end
        
        local dz = (z - terrainZ)
        local alpha = (currentAlpha[whichEffect]*(1 - (shadowAttenuation[whichEffect]*dz))) // 1
        if alpha < 0 then
            alpha = 0
        end
        BlzSetSpecialEffectAlpha(shadow[whichEffect], alpha)

        x = x + shadowOffsetX[whichEffect] + zenithOffsetX*(dz - heightOffset[whichEffect])
        y = y + shadowOffsetY[whichEffect] + zenithOffsetY*(dz - heightOffset[whichEffect])
        BlzSetSpecialEffectPosition(shadow[whichEffect], x, y, GetLocZ(x, y) + shadowOffsetZ[whichEffect])
    end

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

end
Contents

SpecialEffectShadows (Map)

Reviews
Wrda
Back to pending due to confusion. I have no idea what the hell happened, must be been me needing a dose of sleep last night.

Antares

Spell Reviewer
Level 22
Joined
Dec 13, 2009
Messages
523
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 26
Joined
Nov 18, 2012
Messages
1,892
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:
Top