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