• 🏆 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!
  • 🏆 Hive's 6th HD Modeling Contest: Mechanical is now open! Design and model a mechanical creature, mechanized animal, a futuristic robotic being, or anything else your imagination can tinker with! 📅 Submissions close on June 30, 2024. Don't miss this opportunity to let your creativity shine! Enter now and show us your mechanical masterpiece! 🔗 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 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 analog 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.

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
    analog 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
Contents

SpecialEffectShadows (Map)

SpecialEffectShadows (Binary)

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.
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,909
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.
 
Top