| Safe Haskell | None |
|---|---|
| Language | GHC2024 |
Mischief.Render.Tutorial.Start.Shader
Synopsis
1.3. Your First Shader
In this chapter you'll learn to apply a custom material to your sprites.
Defining a Material
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,Bindable) data MyMaterial instanceMaterialMyMaterial where typeVIMyMaterial = Sprite.VertexInputtypeVOMyMaterial = Sprite.VertexOutputtypeBMyMaterial = Bindings type Extends MyMaterial = '[Sprite.SpriteMaterial]vertex=liftExt. (vertex @Sprite.SpriteMaterial)fragment= undefinedfetch= 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:
fetch=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:
fragmentvo = do color <-liftExt$ Sprite.sampleSpritevo.uvpure$ (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!

Your First Buffer
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 (Fieldf (Vec3Float)) deriving (Generic,Bufferable)
Now add it to your Bindings:
newtype Bindings f = Bindings (Uniformf (MyColor f)) deriving (Generic,Bindable)
Next, we need to figure out how we want to fetch this type from the World. The standard way is keeping a as a component on the sprite.Buffer MyColor
So we can set our fetch function to this:
fetch=FetchBindings{ camera = \_ ->pure(), mesh = \sprite ->qgetsprite [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:
fragmentvo = do color <-liftExt$ Sprite.sampleSpritevo.uv Bindings (MyColor myColor) <-bindingspure$ (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:
instanceComponent(MyColorCPU)
And, back on the main module, insert it on the sprites:
import MyMaterial(MyColor(..))
e <-spawn(AnimatorMyAnimator,Anchor$V20.5 0.9,WithMaterial@MyMaterial,def{translation=V3(-60) 0 0}, MyColor @CPU(V31 1 0), -- add this Player )shift(ChangeCharacter Pink) e e <-spawn(AnimatorMyAnimator,Anchor$V20.5 0.9,WithMaterial@MyMaterial,def{translation=V360 0 0}, MyColor @CPU(V30 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!
