packages feed

keid-resource-gltf-0.1.0.0: src/Resource/Gltf/Scene.hs

module Resource.Gltf.Scene where

import RIO

import Codec.GlTF qualified as GlTF
import Codec.GlTF.Mesh qualified as GlTF (MeshIx(..))
import Codec.GlTF.Node qualified as GlTF (Node, NodeIx(..))
import Codec.GlTF.Node qualified as Node
import Codec.GlTF.Root qualified as Root
import Codec.GlTF.Scene qualified as Scene
import Data.Coerce (coerce)
import Data.Tree (Tree)
import Data.Tree qualified as Tree
import Geomancy (Transform(..), quaternion)
import Geomancy.Mat4 qualified as Mat4
import Geomancy.Transform qualified as Transform
import Geomancy.Vec3 qualified as Vec3
import RIO.Vector qualified as Vector

import Resource.Gltf.Model (Mesh, MeshPrimitive, Stuff(..), VertexAttrs(..))

unfoldSceneM
  :: HasLogFunc env
  => Int
  -> Transform
  -> Root.GlTF
  -> Vector Mesh
  -> RIO env (Tree SceneNode)
unfoldSceneM materialOffset initialTransform root allMeshes = do
  (allNodes, initialNodes) <- getRootNodes root
  meshes <- unfoldNodesM allMeshes allNodes (toList initialNodes)
  pure Tree.Node
    { rootLabel = SceneNode
        { snOrigin     = 0
        , snPrimitives = Nothing
        , snNode       = emptyNode
        }
    , subForest =
        map (toSceneNode materialOffset initialTransform) meshes
    }

{-# INLINEABLE toSceneNode #-}
toSceneNode
  :: Int
  -> Transform
  -> Tree (Maybe (Vector MeshPrimitive), GlTF.Node)
  -> Tree SceneNode
toSceneNode materialOffset initialTransform =
  fmap (injectTransforms materialOffset) .
  collectTransforms initialTransform

getRootNodes :: HasLogFunc env => GlTF.GlTF -> RIO env (Vector Node.Node, Vector Node.NodeIx)
getRootNodes root = do
  rootNodes <- case Root.scenes root of
    Nothing -> do
      logWarn "No scenes"
      pure Nothing
    Just scenes ->
      case Vector.toList scenes of
        [] -> do
          logWarn "Empty scene vector"
          pure Nothing
        [one] ->
          pure $ Scene.nodes one
        pick : _rest -> do
          logWarn $ mconcat
            [ "Picking first scene among "
            , display (Vector.length scenes)
            ]
          pure $ Scene.nodes pick

  allNodes <- case Root.nodes root of
    Nothing ->
      throwString "TODO: fallback to raw meshes"
    Just nodes -> do
      pure nodes

  case rootNodes of
    Nothing ->
      throwString "TODO: fallback for lack of scene"
    Just start -> do
      pure (allNodes, start)

emptyNode :: Node.Node
emptyNode = Node.Node
  { camera      = Nothing
  , children    = Nothing
  , skin        = Nothing
  , matrix      = Nothing
  , mesh        = Nothing
  , rotation    = Nothing
  , scale       = Nothing
  , translation = Nothing
  , weights     = Nothing
  , name        = Nothing
  , extensions  = Nothing
  , extras      = Nothing
  }

data LookupError
  = NodeNotFound Int
  | MeshNotFound Int
  deriving (Eq, Ord, Show)

instance Exception LookupError

injectTransforms
  :: Int
  -> ( Maybe (Vector MeshPrimitive)
     , Transform
     , GlTF.Node
     )
  -> SceneNode
injectTransforms materialOffset (mmesh, transform, snNode) = SceneNode{..}
  where
    snOrigin = Vec3.Packed $ Transform.apply 0 transform

    snPrimitives = fmap (Vector.map $ bimap adjustMaterial adjustNode) mmesh

    adjustMaterial = fmap \(materialId, gltfMaterial) ->
      ( materialId + materialOffset
      , gltfMaterial
      )

    adjustNode Stuff{..} = Stuff
      { sPositions = fmap applyTransform sPositions
      , sAttrs     = fmap applyAttrTransform sAttrs
      , sIndices   = sIndices
      }
      where
        applyTransform pos =
          coerce $ Transform.apply (coerce pos) transform

        applyTransformDir dir =
          coerce $ Vec3.normalize $ Transform.apply (coerce dir) (Transform transformDir)
          where
          transformDir = Mat4.transpose . Mat4.inverse $
            Mat4.pointwise (unTransform transform) nullifyTranslation (*)

        applyAttrTransform va = va
          { vaNormal  = applyTransformDir (vaNormal va)
          , vaTangent = applyTransformDir (vaTangent va)
          }

collectTransforms
  :: Transform
  -> Tree (Maybe (Vector MeshPrimitive), GlTF.Node)
  -> Tree (Maybe (Vector MeshPrimitive), Transform, GlTF.Node)
collectTransforms initial root = Tree.unfoldTree go (initial, root)
  where
    go (parent, Tree.Node{rootLabel=(mmesh, node), subForest}) =
      let
        collected = localTransform node <> parent
      in
        ( (mmesh, collected, node)
        , map (collected,) subForest
        )

-- | Build node tree and shed lookup errors as exception.
unfoldNodesM
  :: MonadThrow m
  => Vector Mesh
  -> Vector GlTF.Node
  -> [GlTF.NodeIx]
  -> m [Tree (Maybe (Vector MeshPrimitive), GlTF.Node)]
unfoldNodesM allMeshes allNodes =
  either throwM pure . traverse (inflateNode allMeshes allNodes)

-- | Combine lookup operations for nodes and meshes.
inflateNode
  :: Vector Mesh
  -> Vector GlTF.Node
  -> GlTF.NodeIx
  -> Either LookupError (Tree (Maybe (Vector MeshPrimitive), GlTF.Node))
inflateNode allMeshes allNodes startNode =
  for (unfoldNode allNodes startNode) \getNode -> do
    node <- getNode
    mesh <- getMesh allMeshes node
    pure (mesh, node)

unfoldNode
  :: Vector GlTF.Node
  -> GlTF.NodeIx
  -> Tree (Either LookupError GlTF.Node)
unfoldNode allNodes = Tree.unfoldTree fetch
  where
    fetch (GlTF.NodeIx ix) =
      case allNodes Vector.!? ix of
        Nothing ->
          ( Left $ NodeNotFound ix
          , []
          )
        Just node ->
          ( Right node
          , maybe [] toList $ Node.children node
          )

getMesh
  :: Vector mesh
  -> GlTF.Node
  -> Either LookupError (Maybe mesh)
getMesh allMeshes node =
  case Node.mesh node of
    Nothing ->
      Right Nothing
    Just (GlTF.MeshIx ix) ->
      case allMeshes Vector.!? ix of
        Nothing ->
          Left $ MeshNotFound ix
        Just mesh ->
          Right (Just mesh)

localTransform :: Node.Node -> Transform
localTransform node = mconcat
  [ nodeMatrix
  , nodeScale, nodeRotate, nodeTranslate
  ]
  where
    nodeScale = case Node.scale node of
      Nothing ->
        mempty
      Just (sx, sy, sz) ->
        Transform.scale3 sx sy sz

    nodeTranslate =
      case Node.translation node of
        Nothing ->
          mempty
        Just (tx, ty, tz) ->
          Transform.translate tx ty tz

    nodeRotate = case Node.rotation node of
      Nothing ->
        mempty
      Just (qx, qy, qz, qW) ->
        Transform.rotateQ $ quaternion qW qx qy qz

    nodeMatrix = case Node.matrix node of
      Nothing ->
        mempty
      Just (Node.NodeMatrix memoryBytes) ->
        case Vector.toList memoryBytes of
          [ x1, x2, x3, x4,
            y1, y2, y3, y4,
            z1, z2, z3, z4,
            w1, w2, w3, w4 ] ->
              Mat4.colMajor @Transform
                x1 y1 z1 w1
                x2 y2 z2 w2
                x3 y3 z3 w3
                x4 y4 z4 w4
          _ ->
            error "Node matrix isn't 16-element"

nullifyTranslation :: Mat4.Mat4
nullifyTranslation =
  Mat4.rowMajor
    1 1 1 0
    1 1 1 0
    1 1 1 0
    0 0 0 1

data SceneNode = SceneNode
  { snOrigin     :: ~Vec3.Packed
  , snPrimitives :: Maybe (Vector MeshPrimitive)
  , snNode       :: GlTF.Node
  }
  deriving (Show)