Using Decals

Overview

Recoil provides the ability for games to efficiently draw textures on the map, called decals here.

This guide aims to orient game developers on how to make use of them, the procedure is split in two phases: registering decal textures, using them in game code.

Summary

Register your decal textures on gamedata/resources.lua, example:

{
  graphics = {
    decals = {
      -- bitmaps/decals/01_somedecal.dds, bitmaps/decals/01_somedecal_norm.dds
      "decals/01_somedecal.dds", -- maindecal_1, normdecal_1
      -- bitmaps/decals/02_somedecal.dds, bitmaps/decals/02_somedecal_norm.dds
      "decals/02_anotherdecal.dds", -- maindecal_2, normdecal_2
    }
  }
}

Reference decal names as maindecal_n and normdecal_n, where n is the order they were registered as exemplified above.

Manipulate decals via:

  • Spring.CreateGroundDecal()
  • Spring.DestroyGroundDecal
  • Spring.SetGroundDecal*
  • Spring.GetGroundDecal*

Registering Decal textures

In order to use decals, first you must register their textures with the engine. What this means is that “decals” has its own defined atlas (like “scars”).

In practice, the engine looks for a list of strings in the table returned by gamedata/resources.lua, indexed by .graphics.decals.

Each entry should refer to a texture located in bitmaps/, for the path inside that directory. It also MUST contain a similarly name texture for the normal, see example below:

-- gamedata/resources.lua
return {
  graphics = {
    decals = {
      -- looks for bitmaps/somedecal.dds and bitmaps/somedecal_normal.dds
      -- registers as maindecal_1 and normdecal_1
      "somedecal.dds",
      -- looks for bitmaps/decals/otherdecal.dds and
      -- bitmaps/decals/otherdecal_normal.dds
      -- registers as maindecal_2 and normdecal_2
      "decals/otherdecal.dds"
    }
  }
}

If you don’t have such a file in your game, the one in basecontent is used instead. See VFS Basics for how the engine loads game files.

An example for programmatically adding decals can be found in SplinterFaction/Gamedata/resources.lua at master · SplinterFaction/SplinterFaction · GitHub

Once you have your decal textures registered with the engine you may use them.

Formats

I recommend using DDS, you can use PNG if you must (DDS will look much better from a distance). The engine will even take TGA.

Naming

Notice the registered names for the decals textures are not the same as the ones we passed on .graphics.decals. The engine registers using its own nomenclature and indexing. This can make it difficult to predict what name was registered for a particular texture we want to use.

To make it easier, especially when adding decals programmatically, we recommend indexing your decal files numerically inside a decals folder, so you can always easily refer to the registered texture name using the filename.

For example:

-- gamedata/resources.lua
return {
  graphics = {
    decals = {
      -- looks for <index>_<name>.dds in bitmaps/decals/
      "decals/01_somedecal.dds", -- maindecal_1, normdecal_1
      "decals/02_anotherdecal.dds", -- maindecal_2, normdecal_2
      "decals/03_evenanother.dds", -- maindecal_3, normdecal_3
    }
  }
}

Troubleshooting

You might get a red square where your decal should be, this means something went wrong registering or loading the decal texture.

Here’s a snippet to check which names your decal textures were registered to:

-- This is debug code so that we can see what decals are in the index
for i, v in ipairs(Spring.GetGroundDecalTextures(true)) do
  Spring.Echo("[DECALS] > maindecal_" .. i, v)
end

for i, v in ipairs(Spring.GetGroundDecalTextures(false)) do
  Spring.Echo("[DECALS] > normdecal_" .. i, v)
end

Using Decals in your game

This is probably the easiest part. Make sure you store state for your created decals ids, so they can be cleaned up or further manipulated later.

Simple example

local decalID = Spring.CreateGroundDecal() -- create a new decal

if decalID then
  -- posX, posZ refer to a point in world space
  Spring.SetGroundDecalPosAndDims(decalID, posX, posZ, decalWidth, decalHeight)
  Spring.SetGroundDecalTexture(decalID, "maindecal_1")
  -- Comment below if you don't wish to use a normal map
  Spring.SetGroundDecalTexture(decalID, "normdecal_1", false)
end

-- When your decal is not needed anymore
-- Spring.DestroyGroundDecal(decalID)

You can perform some additional operations on you decal:

-- Applying some random rotation to decal
local angle = math.random() * 2 * math.pi
Spring.SetGroundDecalRotation(decalID, angle)

-- newly created decals have alpha = 1.0 and alphaFallOff = 0
Spring.SetGroundDecalAlpha(decalID, alpha, alphaFallOff)

-- newly created decals have tint values 0.5
-- Note that the actual color applied is `2 * textureColor * tintColor` so 0.5 is effectively no color changes
Spring.SetGroundDecalTint(decalID, tintRed, tintGreen, tintBlue, tintAlpha)

Example use case, metal spots decals

Note

The example below is extracted from the common necessity to draw metal spots.

If you’re looking for the same functionality, see Drawer , Metal Spot Generator , Metal Spot Loader

local decalTextureIndex = "1" -- Uses `maindecal_1` and `normdecal_1`
local decalSize = 85
local offset = { x = 7, z = 7 }

local decalName = "maindecal_" .. decalTextureIndex
local decalNormalName = "normdecal_" .. decalTextureIndex

-- Decal IDs to clean up or operate on later
local decalIDs = {} ---@type integer[]

function widget:Initialize()
  -- A list of tables with fields z and x
  local spots = {} ---@type ({ x: number, z: number })[]

  for _, spot in ipairs(spots) do
    local x, z = spot.x + offset.x, spot.z + offset.z

    local decalID = Spring.CreateGroundDecal()

    if decalID then
      Spring.SetGroundDecalPosAndDims(decalID, x, z, decalSize, decalSize)

      Spring.SetGroundDecalTexture(decalID, decalName, true)
      Spring.SetGroundDecalTexture(decalID, decalNormalName, false)

      -- store the decalID for cleanup on shutdown
      table.insert(decalIDs, decalID)
    end
  end
end

function widget:Shutdown()
  for _, id in ipairs(decalIDs) do
    Spring.DestroyGroundDecal(id)
  end
end

Keeping it Clean

It is a good practice to clean up your decals when the widget exits. You may or may not want this behavior though. If you do, you can simply call Spring.DestroyGroundDecal() in widget:Shutdown()

Like this:

function widget:Shutdown()
  for _, id in ipairs(decalIDs) do
    Spring.DestroyGroundDecal(id)
  end
end