packages feed

keid-render-basic-0.1.8.0: src/Render/Unlit/Sprite/Model.hs

{-# OPTIONS_GHC -fplugin Foreign.Storable.Generic.Plugin #-}

module Render.Unlit.Sprite.Model
  ( InstanceAttrs(..)
  , StorableAttrs
  , InstanceBuffer
  , fromTexture
  , fromAtlas

  , animate_
  ) where

import RIO

import Engine.Vulkan.Format (HasVkFormat(..))
import Engine.Vulkan.Pipeline.Graphics (HasVertexInputBindings(..), instanceFormat)
import Foreign.Storable.Generic (GStorable)
import Geomancy (Vec2, Vec4, UVec2, vec2, vec4, withUVec2, withVec2)
import Geomancy.Vec4 qualified as Vec4
import Render.Samplers qualified as Samplers
import Resource.Buffer qualified as Buffer
import Resource.Image.Atlas (Atlas)
import Resource.Image.Atlas qualified as Atlas
import RIO.Vector.Storable qualified as Storable
import Vulkan.Core10 qualified as Vk
import Vulkan.NamedType ((:::))
import Vulkan.Zero (Zero(..))

data InstanceAttrs = InstanceAttrs
  { vertRect :: Vec4
  , fragRect :: Vec4

  , tint    :: Vec4
  , outline :: Vec4

  , animation :: Vec4 -- direction xy, number of frames, speed

  , textureSize :: UVec2
  , samplerId :: Int32
  , textureId :: Int32
  }
  deriving (Show, Generic)

-- XXX: okay, the layout matches
instance GStorable InstanceAttrs

instance Zero InstanceAttrs where
  zero = InstanceAttrs
    { vertRect = 0
    , fragRect = 0

    , tint    = 0
    , outline = 0

    , animation = 0

    , textureSize = 0
    , samplerId = 0
    , textureId = 0
    }

instance HasVkFormat InstanceAttrs where
  getVkFormat =
    [ Vk.FORMAT_R32G32B32A32_SFLOAT -- Quad scale+offset
    , Vk.FORMAT_R32G32B32A32_SFLOAT -- UV scale+offset
    , Vk.FORMAT_R32G32B32A32_SFLOAT -- Tint
    , Vk.FORMAT_R32G32B32A32_SFLOAT -- Outline (when enabled)

    , Vk.FORMAT_R32G32B32A32_SFLOAT -- Linear animation direction/duration, num frames, phase

    , Vk.FORMAT_R32G32_UINT -- Texture size
    , Vk.FORMAT_R32G32_SINT -- Sampler + texture IDs
    ]

instance HasVertexInputBindings InstanceAttrs where
  vertexInputBindings = [instanceFormat @InstanceAttrs]

type StorableAttrs = Storable.Vector InstanceAttrs
type InstanceBuffer stage = Buffer.Allocated stage InstanceAttrs

fromTexture
  :: Int32 -- ^ Sampler index.
  -> Int32 -- ^ Texture index.
  -> Vec2  -- ^ Sprite size.
  -> Vec2  -- ^ Sprite position.
  -> InstanceAttrs
fromTexture sampler texture wh pos =
  InstanceAttrs
    { vertRect = Vec4.fromVec22 pos wh
    , fragRect = Vec4.fromVec22 0 1

    , tint    = 1
    , outline = 0

    , animation = 0

    , textureSize = 0
    , samplerId = sampler
    , textureId = texture
    }

fromAtlas
  :: Int32 -- ^ Texture ID.
  -> Atlas
  -> Vec2 -- ^ Sprite scale, wrt. to native tile size
  -> Vec2 -- ^ Tile position in atlas tiles. Can be fractional when using subgrids.
  -> Vec2 -- ^ Sprite position.
  -> InstanceAttrs
fromAtlas texture atlas scale atlasPos worldPos =
  InstanceAttrs
    { vertRect = Vec4.fromVec22 worldPos (tileSize * scale)
    , fragRect = Vec4.fromVec22 ((atlasPos + tileMargins / tileSize) * uvScale) uvScale

    , tint    = 1
    , outline = 0

    , animation = 0

    , textureSize = Atlas.sizePx atlas
    , samplerId = Samplers.nearest Samplers.indices
    , textureId = texture
    }

  where
    uvScale = Atlas.uvScale atlas

    tileSize =
      withUVec2 (Atlas.tileSizePx atlas) \tw th ->
        vec2 (fromIntegral tw) (fromIntegral th)

    tileMargins =
      withUVec2 (Atlas.marginPx atlas) \mx my ->
        withVec2 atlasPos \px py ->
          vec2 (fromIntegral mx * px) (fromIntegral my * py)

-- | Simple animation controller with left-to-right linear cycle.
animate_
  :: "margin" ::: Float
  -> "num.frames" ::: Int
  -> "frame duration" ::: Float
  -> "phase" ::: Float
  -> InstanceAttrs
  -> InstanceAttrs
animate_ margin numFrames frameDuration phase attrs = attrs
  { animation =
        vec4
          (fromIntegral numFrames)
          frameDuration
          margin
          phase
  }