In this post I am going to show you how to do multiple passes, how to make your handy HLSL function calls available to more than one shader at a time, how to pass variables to pixel and vertex shaders (in the hlsl pass) while creating a simple planet shader with these elements.
This tutorial seems to have grown and grown, the code for this post I wrote ages ago, but as I am sure you know by my lack of post, I have been very busy and not had much time to write it up and get it posted.
Most of my time has been centred around work and family, and some time on a game I am trying to write with the hope of getting it on the XBLCG, I am aiming to get it into peer review by December, more on that later, back to this post…. So I have had to try and remember what I was thinking as I wrote some of the elements to this, some of it I remember clearly, others not so.
This post is mostly going to try and explain how I came up with my planet shader (see clip) it’s not perfect, but I had fun writing it and I hope that it gives you some ideas for your shaders.
Now this shader did not start off with the intention of being a multi pass shader, but once I got to adding more and more bits to it I found I started to run out of instruction slots and the only way around it that I could think of was to break the shader into more than one pass. So two passes went to three, three went to four and finally four to five.
How do we go about creating a multi-pass shader? Well that’s quite simple, you just have more than one pass in your technique. The initial issue that I had was that my last technique over wrote the one before it so I discovered that I needed to blend the passes. This is done like any other blending the only difference is that it has to be done in the pass it’s self. Some passes needed an alpha blend others an additive blend.
I am not going to go into too much detail on the code in the pixel and vertex shaders as it’s mostly covered in the previous episodes, I’ll mostly cover the passes.
First Pass
In this pass we just do the colour, bump, glow and reflection shading giving us a basic planet, later when we add the clouds we will go back to the colour pixel shader we will add the shadows to this pass for the clouds. So, our first pass looks like this:
pass
Colors
{
AlphaBlendEnable = False;
CullMode = CCW;
VertexShader = compile vs_2_0 VS_Color();
PixelShader = compile ps_2_0 PS_Color();
}
{
AlphaBlendEnable = False;
CullMode = CCW;
VertexShader = compile vs_2_0 VS_Color();
PixelShader = compile ps_2_0 PS_Color();
}
As you can see I am setting some render states in this pass. I am just making sure the alpha blending is off and that the cull mode is counter clock wise, then the regular vertex shader is called followed by the pixel shader.
Second Pass
In the second pass I want to add some ripples to the oceans. The geometry is already drawn in the first pass so all I need to do is call my shader to apply the waves to the oceans, bus as I have said above, I need to blend the two pixel shaders to get my effect.
pass
Waves
{
AlphaBlendEnable = True;
SrcBlend = SrcAlpha;
DestBlend = One;
PixelShader = compile ps_2_0 PS_Water();
}
{
AlphaBlendEnable = True;
SrcBlend = SrcAlpha;
DestBlend = One;
PixelShader = compile ps_2_0 PS_Water();
}
So, we switch on alpha blending, then do an additive blend. I can then call my water pixel shader to apply the ripples. If you look realy close, the ripples move across the surface of the planet, I have a function that alters the text coords of the ripples to do this. To show that you can share functions across shaders I put this function in another file, how this is accessed I will cover later.
Third Pass
In this pass I want to add my clouds, to do this I am going to draw the geometry again, this time slightly larger than the planets surface, that way my clouds will appear a little raised from plants surface. I also wanted so shadows to appear on the surface of the planet cast by the clouds. To do this I went back to the cloud pixel shader and added the cloud textures to the final colour calculation but rendered them based on a shadow intensity. To “animate” the clouds I rotate the texture around the planet, again there is a shader parameter you can use to control the speed and I have a function to move the text coords of the cloud texture to do this and again, this is in my seperate function file.
pass
Clouds
{
// Already set, no need to set again, keep them incase I move the passes about though.
//AlphaBlendEnable = True;
//SrcBlend = SrcAlpha;
//DestBlend = One;
VertexShader = compile vs_2_0 VS_OuterAtmoshpere(.02);
PixelShader = compile ps_2_0 PS_Cloud();
}
{
// Already set, no need to set again, keep them incase I move the passes about though.
//AlphaBlendEnable = True;
//SrcBlend = SrcAlpha;
//DestBlend = One;
VertexShader = compile vs_2_0 VS_OuterAtmoshpere(.02);
PixelShader = compile ps_2_0 PS_Cloud();
}
As you can see the vertex shader here is passed a parameter, this parameter is how much bigger I want to draw the new geometry. The blending is set in the previous pass so no need to set it again, I have kept the code there though in case I decide to move it.
VS_OUT2
VS_OuterAtmoshpere(VS_IN input,uniform float size)
{
VS_OUT2 output = (VS_OUT2)0;
output.Normal = mul(input.Normal, world);
output.Position = mul(input.Position, wvp) + (mul(size, mul(input.Normal, wvp)));
output.TexCoord = input.TexCoord;
output.pos = mul(input.Position,world);
return output;
}
{
VS_OUT2 output = (VS_OUT2)0;
output.Normal = mul(input.Normal, world);
output.Position = mul(input.Position, wvp) + (mul(size, mul(input.Normal, wvp)));
output.TexCoord = input.TexCoord;
output.pos = mul(input.Position,world);
return output;
}
As you can see, you need to declare the size parameter as a uniform in order to have it as an internal parameter. As you can see the position the current vertex is draw at is moved out along the direction of the normal by the distance of the size passed in, so drawing me a larger model around the original.
Forth Pass
In this pass I attempt to fake atmosphere, I don’t need to render the geometry again as I can use the geometry the cloud pass has just created, this time though I use an alpha blend to get the effect I want.
pass
OuterAtmoshpere
{
//AlphaBlendEnable = True;
//SrcBlend = SrcAlpha;
DestBlend = InvSrcAlpha;
// No need to move it out again, can use the same geom as the clouds.
//VertexShader = compile vs_2_0 VS_OuterAtmoshpere(.02);
PixelShader = compile ps_2_0 PS_OuterAtmoshpere(true);
}
{
//AlphaBlendEnable = True;
//SrcBlend = SrcAlpha;
DestBlend = InvSrcAlpha;
// No need to move it out again, can use the same geom as the clouds.
//VertexShader = compile vs_2_0 VS_OuterAtmoshpere(.02);
PixelShader = compile ps_2_0 PS_OuterAtmoshpere(true);
}
You can see in the pixel shader I pass a Boolean, this is so I only had to write one pixel shader for both the inner atmosphere and the atmosphere halo.
Fifth Pass
So, onto the final pass now, the atmosphere halo. This works pretty much the same as the previous pass, only we set the cull mode to be clock wise. This results in the the sphere being rendered inside out, I also render the sphere much larger.
pass
UpperOuterAtmoshpere
{
//AlphaBlendEnable = True;
//SrcBlend = SrcAlpha;
//DestBlend = InvSrcAlpha;
VertexShader = compile vs_2_0 VS_OuterAtmoshpere(.2);
PixelShader = compile ps_2_0 PS_OuterAtmoshpere(false);
CullMode = CW;
}
{
//AlphaBlendEnable = True;
//SrcBlend = SrcAlpha;
//DestBlend = InvSrcAlpha;
VertexShader = compile vs_2_0 VS_OuterAtmoshpere(.2);
PixelShader = compile ps_2_0 PS_OuterAtmoshpere(false);
CullMode = CW;
}
In the class used to render the planet, I make sure that the cull mode is set back to counter clock wise cull after the render.
Shader Header
This is where I have put the functions to rotate and to revolve the texture coordinates, I did this to show how you don’t have to keep writing the same functions in each of your shaders if they are common across your shaders. So to have access to these functions in my shader I just do this (if you are a C/C++ programmer this will look very familiar :P)
#include
"ShaderTools.fxh"
Simple as that….
You can find the solution for this post here.
The link to the code for this post doesn't seem to work any more. Any chance of getting an updated link to the code? Thanks.
ReplyDeleteHi Wilber,
DeleteNone of the links to the source code will work here, this blog is an archive of my blog on the XNA-UK user group whish is now no more.
I do have the original download(s) stored on a server so a people ask for them I'll patch the links up, so if you come back here a few hours or so after you read this, then the link to this dld should be live.
Regards,
Charles.
I have a question, how do you make sure that in step two the ripples are not drawn on top of the continent? not quite sure to understand that part.
DeleteHi Mystic River,
DeleteThe shader is using the specular map to know where the water is, that's why it does not put the ripples on the continent, as those bits on the specular map are black..
Thanks! now it makes more sense to me , still need to learn a lot more to understand your code :)
ReplyDeleteSo , let me confirm this just in case. You call in your second pass PS_OUT PS_Water(VS_OUT input), where input contains postion, texture coordinate, light and some other stuff defined in your VS_OUT struct, which was filled by your first pass Colors. The output of this function will be blended with the framebuffer by using the blending mode :
AlphaBlendEnable = True;
SrcBlend = SrcAlpha;
DestBlend = One;
so you basically use the light information (specular map) to basically saturate the color, when is 0 then no diffuse color hence not being drawn on the continent.
VS_IN , VS_OUT , VS_OUT2 and PS_OUT are structures that you use in your passes and I would assume that the GPU will fill up and keep the values of all those structs per pixel level for the next pass, the thing I do not understand at this point is how does the GPU knows that it has to store all of this?
Sorry for my long post, but I am writing some pixel shaders and I do have a very confusing/slow code on hand and trying to think differently, this tutorial gave me a lot of things to think about, and most probably what I am doing needs a lot of rework, thanks!
The pixel shader is writing to the render target, so in the resulting passes I am telling the shader to blend the pixel at the current location with the new one from the pixel shader function. If I didn't specify the blend, it would just write directly over the first pass's pixel.
DeleteHello Charles, Thanks for keeping these tutorials up on blogger, they are invaluable!
ReplyDeleteI have a question regarding the cloud shadows, specifically: "I went back to the cloud pixel shader and added the cloud textures to the final colour calculation but rendered them based on a shadow intensity."
I'm still unclear how you darkened the underlying surface from within the cloud pass. Any modifications I make to the final RGB in the cloud pass affects only the cloud color. I'm working with Unity shaderlab passes (Pass tag: "LightMode" = "Always").
Hi John,
DeleteIt's been a long time since I have looked at this code lol, I have however created this in Unity myself already, and might even do it as an asset on the Asset Store.
https://www.youtube.com/watch?v=rUX7z5H28n0
Happy to share the code with you though if you like, post your email address in a comment here, I won't let the comment be shown, but Ill send you a contact email and I can share the shader with you (If I can find it :P)
Regards,
Charles.
Hey, thanks for the quick reply! I wound up multiplying the final diffuse of the surface pass by one minus the cloud texture.(so the first pass is doing a texture lookup on the cloud texture, for that purpose alone). I dunno how it will fare on mobile, but your article has given me lots of ideas and places to start :) (Your method for handling the night time lights seems very elegant, after having read articles about branching fragment logic and texture lerps!).
DeleteI also created a version that just offsets the uvs of the "shadow", and removed the mesh normal duplication. It seems ok for the amount of screen space they will be taking up. It has some limitations (e.g: the orientation of the uv shells matters), but seems passable!
It turns out I'm already following you on youtube, it's a small world when it comes to tech art! I'll follow you on google plus as well, if that's alright.