mischief-render-0.1.0.0: src/Mischief/Render/Tutorial/Materials.hs
{-# OPTIONS_GHC -Wno-unused-imports #-}
-- |
-- [Previous Chapter: Shaders]("Mischief.Render.Tutorial.Shaders")
--
-- [Main Page]("Mischief.Render")
module Mischief.Render.Tutorial.Materials
( -- * 2.3. Materials
-- $intro
-- * Vertex Input and Output
-- $vivo
-- * Bindings
-- $bindings
-- * Buffers
-- $buffers
-- * Running
-- $running
-- * Fullscreen
-- $fullscreen
-- * Postprocessing
-- $postprocessing
)
where
import Mischief.Animation
import Mischief.ECS
import Mischief.Materials qualified as Mat
import Mischief.Materials.Fullscreen qualified as Full
import Mischief.Materials.Sprite qualified as Sprite
import Mischief.Math
import Mischief.Render
-- $intro
-- This chapter will walk you through the important notions and tools for writing Materials.
--
-- A Material is esentially the shader that renders objects from the world onto a texture (generally a camera's texture).
--
-- A Material can be applied to an entity by inserting a @WithMaterial@ component on it:
--
-- @
-- 'insert' ('WithMaterial' \@'Sprite.SpriteMaterial') bob
-- @
--
-- Each material has an associated VertexInput (@VI@), a VertexOutput (@VO@), and a set of Bindings (@B@).
-- $running
-- To \'run\' a material, use one of the @'renderMaterial'@ and @'renderSortedMaterial'@ functions. These are typically
-- scheduled to run each frame, in a Render schedule.
--
-- @
-- 'systems' ('renderMaterial' \@MyMaterial)
-- & 'schedule' \@'RenderUpdate'
-- @
-- $vivo
-- @VI@ is the input to the vertex shader. It contains a @Uint@, the index of the current vertex. (other types of draw calls will be supported in the future). This is the standard way of writing it:
--
-- @
-- newtype VertexInput f = VertexInput ('Mat.BuiltIn' f \"vertex_index\" Uint)
-- deriving ('Generic', 'Mat.ShaderParam')
-- @
--
-- @VO@ is the output of the vertex shader and the input to the fragment shader. It always contains the builtin parameter @position@, indicating the world position of the vertex. The other parameters
-- are added with @Location@ and a unique index. A common @VO@ looks something like this:
--
-- @
-- data VertexOutput f = VertexOutput
-- { pos :: 'Mat.BuiltIn' f \"position\" ('Mat.Vec4' 'Mat.Float'),
-- uv :: 'Mat.Location' f 0 ('Mat.Vec2' 'Mat.Float')
-- }
-- deriving ('Generic', 'Mat.ShaderParam')
-- @
--
-- The vertex and fragment shaders of material @M@ are two functions that look like this:
--
-- @
-- vertex :: 'Mat.VI' M 'GPU' -> 'Mat.Shader' M ('Mat.VO' M 'GPU')
-- @
--
-- @
-- fragment :: 'Mat.VO' M 'GPU'-> 'Mat.Shader' M ('Mat.Vec4' 'Mat.Float')
-- @
--
-- The 'GPU' applied on the types is their shader representation. The above types become:
--
-- @
-- VertexInput(Uint)
-- @
--
-- @
-- VertexOutput {
-- pos :: 'Mat.Vec4' 'Mat.Float',
-- uv :: 'Mat.Vec2' 'Mat.Float'
-- }
-- @
-- $bindings
-- The Bindings of a Material are data that's passed into the shaders from outside. Bindings can contain Textures, Samplers and Buffers.
--
-- @
-- data Bindings f = Bindings {
-- texture :: 'Mat.Binding' f 'Mat.Texture'
-- sampler :: 'Mat.Binding' f 'Mat.Texture',
-- buffer :: 'Mat.Uniform' f SomeBuffer
-- } deriving ('Generic', 'Mat.Bindable')
-- @
--
-- Each Material has a @fetch@ function which is used to specify how you want to fetch the binding data from the world. This is done through
-- a @'Mat.FetchBindings'@ type which contains 3 functions:
--
-- @
-- camera :: [CameraEntity]('Entity') -> 'Query' 'System' x
-- @
--
-- @
-- mesh :: [MeshEntity]('Entity') -> 'Query' 'System' y
-- @
--
-- @
-- result :: x -> y -> b 'CPU'
-- @
--
-- A function grabs some of the data from the camera, one from the mesh (i.e. sprite), and one which combines them to make the Bindings.
--
-- As an example, look at the Bindings and fetch function of @'Sprite.SpriteMaterial'@:
--
-- @
-- data Bindings f = Bindings
-- { matrices :: 'Mat.Uniform' f ('Mischief.Render.Camera.Matrices' f),
-- sprite :: 'Mat.Uniform' f ('Sprite.SpriteData' f),
-- texture :: 'Mat.Binding' f 'Mat.Texture',
-- sampler :: 'Mat.Binding' f 'Mat.Sampler'
-- }
-- deriving ('Generic', 'Mat.Bindable')
-- @
--
-- @
-- fetch =
-- 'Mat.FetchBindings'
-- ( \\camera -> do
-- (DefaultSampler sampler) <- 'qres' \@DefaultSampler
-- CameraMatrices matrices <- 'qget' camera [qd|CameraMatrices|]
-- 'pure' (matrices, sampler)
-- )
-- ( \\sprite -> do
-- ('Sprite' {image}, buffer) <- 'qget' sprite ['qd'|Sprite, (Buffer SpriteData)|]
-- ImageTextureView texture <- 'qget' image [qd|ImageTextureView|]
-- 'pure' (buffer, texture)
-- )
-- (\\(matrices, sampler) (buffer, texture) -\> Bindings {matrices, sampler, sprite = buffer, texture})
-- @
--
-- Inside a @Shader M@, you can request the bindings of material @M@ using @bindings@:
--
-- @
-- b <- 'Mat.bindings'
-- @
--
-- You can also get the bindings of any material which the current material extends using @extension@:
--
-- @
-- b <- 'Mat.extension' \@'Mischief.Materials.Sprite.SpriteMaterial'
-- @
-- $buffers
-- Buffers are data types made of shader primitives, such as:
--
-- @
-- data SpriteData f = SpriteData
-- { coords :: 'Mat.Field' f ('Mat.Vec3' 'Mat.Float'),
-- size :: 'Mat.Field' f ('Mat.Vec2' 'Mat.Float'),
-- slice :: 'Mat.Field' f ('Mat.Vec4' 'Mat.Float'),
-- flip :: 'Mat.Field' f ('Mat.Vec2' 'Mat.Int'),
-- rotation :: 'Mat.Field' f ('Mat.Mat' 3 3)
-- }
-- deriving ('Generic', 'Mat.Bufferable')
-- @
--
-- This is how you create buffers:
--
-- @
-- 'Just' device <- 'res' \@'RenderDevice'
-- buf <- 'createBuffer' \@SpriteData device
-- @
--
-- And how you update them:
--
-- @
-- 'Just' queue <- 'res' \@'RenderQueue'
-- 'uploadBuffer' queue buf SpriteData {...}
-- @
--
-- When uploading, you must use the @'CPU'@ representation of the type. This uses @"Linear"@ types to represent vectors and matrices. For instance, @SpriteData CPU@ looks like this:
--
-- @
-- SpriteData {
-- coords :: 'V3' 'Float'
-- size :: 'V2' 'Float'
-- ...
-- }
-- @
--
-- Each buffer can be inserted on entities as a component, which makes it easy to fetch them.
--
-- If your buffer's CPU representation is also a component, you can schedule @maintainBuffer@. For instance:
-- for instance:
--
-- @
-- instance 'Component' (SpriteData 'CPU')
-- @
--
-- @
-- 'systems' ('maintainBuffer' \@SpriteMaterial)
-- & 'schedule' \@'RenderPrepare'
-- @
--
-- For all entities that have @SpriteData CPU@, it will ensure a @Buffer SpriteData@ component is on them and upload data into it
-- each time it detects a change.
-- $fullscreen
-- To make a fullscreen material, make a material that extends 'Full.FullscreenMaterial' and uses its vertex shader. For instance, to make a material which makes the screen red:
--
-- @
-- data Bindings f = Bindings deriving ('Generic', 'Mat.Bindable')
-- data RedMaterial
--
-- instance 'Mat.Material' RedMaterial where
-- type 'Mat.VI' RedMaterial = Fullscreen.'Full.VertexInput'
-- type 'Mat.VO' RedMaterial = Fullscreen.'Full.VertexOutput'
-- type 'Mat.B' RedMaterial = Bindings
--
-- type 'Mat.Extends' RedMaterial = \'['Full.FullscreenMaterial']
--
-- vertex = 'Mat.liftExt' . 'Mat.vertex' \@'Full.FullscreenMaterial'
-- fragment _ = pure $ 'Mat.vec4' 1 0 0 1
--
-- fetch =
-- 'Mat.FetchBindings'
-- ('const' $ 'pure' ())
-- ('const' $ 'pure' ())
-- ('const' $ 'const' $ Bindings)
-- @
--
-- Note that, when rendering, you need to set the number of vertices to 4:
--
-- @
-- 'renderMaterial' \@RedMaterial 'def' {'nvertices' = 4}
-- @
-- $postprocessing
-- A postprocess material is a fullscreen material which also takes its camera's texture as an input, modifying it.
--
-- You can turn the above material into a postprocess material by changing its bindings and its fetch function:
--
-- @
-- data Bindings f = Bindings
-- { texture :: 'Mat.Binding' f 'Mat.Texture',
-- sampler :: 'Mat.Binding' f 'Mat.Sampler'
-- }
-- deriving ('Generic', 'Mat.Bindable')
-- @
--
-- @
-- fetch =
-- 'Mat.FetchBindings'
-- ( \\camera -> do
-- (DefaultSampler sampler) <- 'qres' \@DefaultSampler
-- texture <- 'cameraSwapView' camera
-- 'pure' (sampler, texture)
-- )
-- ('const' $ 'pure' ())
-- (\\(sampler, texture) _ -> Bindings texture sampler)
-- @
--
-- You can now, for instance, multiply the entire camera's output with red:
--
-- @
-- fragment vo = do
-- Bindings {texture, sampler} <- 'Mat.bindings'
-- 'pure' $ 'Mat.sample' texture sampler vo.uv * 'Mat.vec4' 1 0 0 1
-- @