mischief-render-0.1.0.0: src/Mischief/Render/Tutorial/Start/Shader.hs
{-# OPTIONS_GHC -Wno-unused-imports #-}
-- |
-- [Previous Chapter: Your First Animation]("Mischief.Render.Tutorial.Start.Animation")
--
-- [Next Chapter: Basics]("Mischief.Render.Tutorial.Basics")
--
-- [Main Page]("Mischief.Render")
module Mischief.Render.Tutorial.Start.Shader
( -- * 1.3. Your First Shader
-- $intro
-- * Defining a Material
-- $def
-- * Your First Buffer
-- $buf
-- * [Next Chapter: Basics]("Mischief.Render.Tutorial.Basics")
)
where
import Mischief.Animation
import Mischief.ECS
import Mischief.Materials qualified as Mat
import Mischief.Materials.Sprite qualified as Sprite
import Mischief.Math
import Mischief.Render
-- $intro
-- In this chapter you'll learn to apply a custom material to your sprites.
-- $def
-- Typically materials are written in separate modules, due to needing their own imports and requiring you to disable the Prelude. So make a new module
-- called @MyMaterial@ (don't forget to add it to your @.cabal@).
--
-- Now copy this code into it:
--
-- @
-- import "Mischief.Materials"
-- import "Mischief.Materials.Sprite" qualified as Sprite
-- import "Prelude" ()
--
-- data Bindings f = Bindings deriving ('Generic', 'Mat.Bindable')
--
-- data MyMaterial
--
-- instance 'Mat.Material' MyMaterial where
-- type 'Mat.VI' MyMaterial = Sprite.'Sprite.VertexInput'
-- type 'Mat.VO' MyMaterial = Sprite.'Sprite.VertexOutput'
--
-- type 'Mat.B' MyMaterial = Bindings
--
-- type Extends MyMaterial = '[Sprite.'Sprite.SpriteMaterial']
--
-- 'Mat.vertex' = 'Mat.liftExt' . (vertex @Sprite.'Sprite.SpriteMaterial')
--
-- 'Mat.fragment' = undefined
--
-- 'Mat.fetch' = undefined
-- @
--
-- As you can see, the material has 3 types, one corresponding to the vertex input, one to the output, and one to the bindings: the data we're passing into the shader.
--
-- For the first two we're just using the ones that @SpriteMaterial@ uses (check the source code over there to see how they're defined),
-- while for the Bindings we've just defined our own data type that contains nothing, for now.
--
-- Each material also needs a vertex and a fragment shader. A vertex shader is responsible for determining the UVs and the placement of vertices, while
-- a fragment shader colors each pixel of the mesh. We are using @liftExt@ to lift the vertex shader from the sprite material (possible only if we extend it). The fragment
-- shader we'll write out in a moment.
--
-- There's also a @fetch@ function which is responsible for grabbing the Bindings from the World. For now we can just set it to this:
--
-- @
-- 'Mat.fetch' =
-- 'Mat.FetchBindings'
-- { camera = \\_ -> 'pure' (),
-- mesh = \\_ -> 'pure' (),
-- result = \\_ _ -> Bindings
-- }
-- @
--
-- We are not grabbing any data from the mesh or from the camera, as we don't need it to construct our Bindings.
--
-- Now, for the fragment shader, we'll just sample our sprite and inverse its colors:
--
-- @
-- 'Mat.fragment' vo = do
-- color <- 'Mat.liftExt' $ Sprite.'Sprite.sampleSprite' vo.uv
-- 'pure' $ (1 -^ color.rgb) +: color.a
-- @
--
-- @-^@ is what we use to subtract a vector (right) from a scalar (left). You'll see this with a lot of shader operators, it's a convention inspired by "Linear".
-- @+:@ is used to append a scalar to the end of a vector. So that whole operation inverses the rgb values of the sprite's color, while keeping the alpha intact.
--
-- Now you just need to set your sprites to use this instead of the current material. Import it into your previous module:
--
-- @
-- import MyMaterial(MyMaterial)
-- @
--
-- And then replace all occurences of @SpriteMaterial@ with @MyMaterial@. That's it!
--
-- 
-- $buf
-- Let's pass some data into the shader!
--
-- First, we'll need to define a Bufferable type. That is, a type that can be sent to the GPU. Add this to your material module:
--
-- @
-- newtype MyColor f = MyColor ('Mat.Field' f ('Mat.Vec3' 'Mat.Float')) deriving ('Generic', 'Mat.Bufferable')
-- @
--
-- Now add it to your Bindings:
--
-- @
-- newtype Bindings f = Bindings ('Mat.Uniform' f (MyColor f)) deriving ('Generic', 'Mat.Bindable')
-- @
--
-- Next, we need to figure out how we want to fetch this type from the World. The standard way is keeping a @'Buffer' MyColor@ as a component on the sprite.
--
-- So we can set our @fetch@ function to this:
--
-- @
-- 'Mat.fetch' =
-- 'Mat.FetchBindings'
-- { camera = \\_ -> 'pure' (),
-- mesh = \\sprite -> 'qget' sprite ['qd'|(Buffer MyColor)|],
-- result = \\_ color -> Bindings color
-- }
-- @
--
-- We'll also modify our fragment shader to use this custom color. For instance, we can multiply it over the sprite's color:
--
-- @
-- 'Mat.fragment' vo = do
-- color <- 'liftExt' $ Sprite.'Sprite.sampleSprite' vo.uv
-- Bindings (MyColor myColor) <- 'Mat.bindings'
-- 'pure' $ (myColor * color.rgb) +: color.a
-- @
--
-- The last step is creating the buffers and filling them with the correct data. The easiest way is to make the CPU-side @MyColor@ into a component:
--
-- @
-- instance 'Component' (MyColor 'CPU')
-- @
--
-- And, back on the main module, insert it on the sprites:
--
-- @
-- import MyMaterial(MyColor(..))
-- @
--
-- @
-- e <-
-- 'spawn'
-- ( 'Animator' MyAnimator,
-- 'Anchor' $ 'V2' 0.5 0.9,
-- 'WithMaterial' \@MyMaterial,
-- 'def' {'translation' = 'V3' (-60) 0 0},
-- MyColor \@'CPU' ('V3' 1 1 0), -- add this
-- Player
-- )
--
-- 'shift' (ChangeCharacter Pink) e
--
-- e <-
-- 'spawn'
-- ( 'Animator' MyAnimator,
-- 'Anchor' $ 'V2' 0.5 0.9,
-- 'WithMaterial' \@MyMaterial,
-- 'def' {'translation' = 'V3' 60 0 0},
-- MyColor \@'CPU' ('V3' 0 1 1) -- and this
-- )
--
-- 'shift' (ChangeCharacter Green) e
-- @
--
-- We can use the @maintainBuffer@ system to automatically upload our data from the CPU component to the buffer. Uploading operations
-- are typically ran in @RenderPrepare@.
--
-- @
-- systems ('maintainBuffer' \@MyColor)
-- & 'schedule' \@'RenderPrepare'
-- @
--
-- All done!
--
-- 