{-# 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 -- @