Let There Be Light (And Shadow) | Writing Unity URP Code Shaders Tutorial [2/9] ✔️ 2021.3

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hi i'm ned and i make games have you ever wondered how lighting and shadows work in unity or do you want to write your own shaders for the universal render pipeline but without the shader graph either because you need some special feature or just prefer writing code by hand this tutorial has you covered in fact this is the second part in a series about writing hlsl shaders for urp in this video i'll show how to add lighting to a shader this includes a simple explanation of shadow mapping how objects cast and receive shadows in urp as well as an introduction to keywords and shader variants an important concept when writing any type of shader if you prefer written tutorials check out the article version of this tutorial in the video description you'll also find links to the rest of the tutorials in this series if you're starting here i would recommend watching the first video in the series where we write a basic unlit shader this video will continue directly from it before i move on i want to thank all my patrons for helping make this series possible and give a big shout out to my next gen patron croobiedoobydoo thank you all so much and with that let's get started so far we've learned how to write unlit shaders or shaders not affected by any light obviously lighting is a very important aspect of rendering and programmers devote a lot of shader code to it luckily for us eurp provides a helper function which deals with much of it in urp's lighting.hlsl file there is a function called universal fragment blinnfong it computes a standard lighting algorithm called the blendfong lighting model blinfong is actually composed of two components the first calculates diffuse lighting which eliminates the side of an object facing towards a light the second computes specular lighting the shine or highlight that brings a smooth object to life open myletforwardlitpass.hlsl and in the fragment function call universal fragment blindfold it returns a color which we can just return as well universal fragment blen fong takes many arguments but to keep things neat it bundles them up into two structures the first called input data holds information about the position and orientation of the mesh at the current fragment the second called surface data holds information about the surface material's physical properties like color define a variable for both these structures have nearly a dozen fields each but we don't need to set them all yet unlike c sharp structure fields must be manually initialized to set all fields to 0 cast a 0 to the structure type this looks really strange but it's an easy way to initialize this structure without having to know all of its fields now pass input data and surface data to universal fragment blindfold back in part 1 i mentioned that there are several differences between unity 2020 and unity 2021 well here's the first that affects our shader in unity 2020 urp does not have an overload of universal fragment blend fong that takes a surface data structure you'll have to pass in the fields individually like this for now don't worry about what each field means we'll get to all of them soon both to keep this tutorial more organized and to help you upgrade projects in the future i want the same code to run in unity 2020 and unity 2021 thankfully there's an easy way to run different code depending on the current unity version you might have seen a hash if preprocessor command in c sharp usually it omits code that should only run in the editor hash if is also available in shader lab and hlsl where it's a common site if the expression following hash f is true then the code in between the hash if and the hash endif will compile otherwise the compiler ignores it hash f can only depend on values that are known before compiling code like constants and number literals unity provides a constant called unityversion which contains the current unity version as an integer basically the version number with periods omitted so in our fragment function we want to switch between passing the surface data struct or its individual fields based on the unity version if the version is greater than or equal to 202 102 we can pass the structure to define an else block which works just like you'd expect use hash else inside call the version with individual arguments now our shader code will dynamically change depending on which unity version we're working with in unity 2020 it'll look like this in 2021 it'll look like this pretty cool for the future if you need to support another possibility you can use a hash alif which is short for else if here's an example for a hypothetical unity 2030.3 version but let's stick with the present check out your shader in the scene editor it's now just a black sphere to get back to where we were before we need to set some data into the input structs from the color properties we can set albedo and alpha which are fancy names for base color and transparency but remember that the shader doesn't support transparency just yet so don't really expect any next we need something called a normal vector you may have heard of normal vectors from math or unity's physics systems but they're just vectors that point directly outwards from a surface blindfold uses them to find where the mesh faces a light source normal vectors apply to faces on the mesh but they're organized into a mesh vertex stream like position or uvs this can complicate things there's no problem on a sphere but on sharp cornered meshes like this cube it can look like vertices have multiple normal vectors in reality unity duplicates vertices one for each normal vector this way one normal corresponds to exactly one vertex and the normal always matches the face that's connected to it regardless the input assembler will take care of gathering normal data add a new field to the attribute struct tagged with the normal semantic these normals are also an object space like position when adding a new data source to a shader it's useful to plan out its journey through your code blindfong uses normals in the fragment stage but they're only accessible through the input assembler we need to pass them through the vertex stage and interpolate them with the rasterizer in addition universal blindfold expects normals in world space and it's necessary to transform them at some point we could do that in the fragment stage but we don't need object space normals there at all it's a bit more optimal to calculate world space normals in the vertex function since it runs fewer times than the fragment function using this plan go through the code section by section and modify it as needed we already added a normal os field to attributes add a normal ws field to the interpolator struct the rasterizer will interpolate any field tagged with a text chord semantic so tag normal with text chord 1. why 1 and not 0 well tech scored 0 is already taken by uvs and 2 fields can't have the same semantic the rasterizer can handle a bunch of text chord variables so 2 is no problem in the vertex function transform the normal vector from object space to world space urp provides another function to do this similar to the one that we use for positions call it and set the world space normal in the interpolator struct in the fragment function set the normal in the input data struct before moving on let's think a little about what happens to the normal vector when it's interpolated when the rasterizer interpolates vectors it interpolates each component individually this can cause a vector's length to change like in this example for lining to look its best all normal vectors must have a length of 1. this requirement is common when a vector encodes a direction we can bring any vector to a length of 1 using the aptly named normalized function normalize is kind of slow since it has an expensive square root calculation inside i think this step is definitely worth it for smoother lighting it's especially noticeable on specular highlights but if you're pressed for processing power you can skip it let's not hear though in the scene editor we finally have lighting but it looks a little flat with only diffuse lighting for specular highlights urp needs a little more data specifically world space position right now the fragment function does not have access to world space position only pixel positions there's not an easy way to transform these back to world space it's best to pass it as another field in the interpolator struct tag it with another free text chord variable note that i reorganized them here a little bit just for personal preference set position in the vertex stage using urp's handy transform function then in the fragment function set position ws and input data no need to normalize here of course since position is not a direction and can have any length if you move around an object using the default lit shader you'll notice that highlights also move slightly this is because specular lighting depends on the view direction or the direction from the fragment to the camera we can calculate this in the fragment function from the world space position using another urp function call it then set view direction world space and input data highlights can sometimes have different colors than albedo and urp allows you to specify this with a specular field in the surface data struct for now just set it to white if you take a peek at the scene you'll still see no highlights it turns out that universal fragment blend fong uses a hash shift command internally to toggle highlights on and off it uses a special type of constant called a keyword to do so keywords are sort of like boolean constants that you can enable using a hash define command shaders make extensive use of keywords to turn on and off different features it's faster to disable specular lighting instead of for instance setting specular color to black either option has the same visual effect but not evaluating something is obviously quicker than throwing out the result however i do want specular lighting in this shader for organization i define keywords in the shader lab file for each pass making it obvious which keywords are enabled at a glance add hash define specular color to your past block finally highlights but they're pretty big your p provides an easy way to shrink them using a value called smoothness the higher the smoothness the smaller the highlight visualize a perfectly smooth metal ball the highlight is pretty focused for now let's define smoothness using a material property add a property called smoothness of the float type to your shader in my lit forward pass declare it at the top of the file and then set it to the surface data structure using the material inspector you can control the size of the highlight using the smoothness property note that smoothness works differently depending on the unity version 2021's implementation is much more sensitive this is just a consequence of how urp calculates lighting behind the scenes one note before we move on the shader only supports the main light for now we should get the basics down before complicating things with additional lights but i will show how to add support for them in part 5 of the series so far we've worked with just one object if you create another you'll notice that objects with our shader neither cast nor receive shadows these are two separate concepts in the world of shaders and we'll need to implement them both first let's investigate how urp handles shadows with an algorithm called shadow mapping the end goal is to find a cheap way to check if a fragment is in shadow with respect to any light source again let's only consider the main light a naive approach is to check for an object between the fragment and the light this is very slow since the shader would have to execute a raycast looping through all objects in the scene there's got to be a faster way first let's restructure our algorithm to orient the ray starting from the light and then shooting out in a straight line intersecting our fragment and any other surfaces on the same line second notice that only one surface on the ray is lit for every surface but the one closest to the light there is an object between it and the light to determine if a fragment is in shadow we can simply test if the distance to the light is greater than the minimum distance among all surfaces to the light this reduces the problem to finding the distance from the light to the closest surface along all light rays if you think about it this kind of sounds familiar when rendering we draw the color of the closest surface to the camera along all view rays switch color with a distance and the camera with the light and we're in business how can we draw a distance remember that colors are just numbers so we can store distance inside the red channel of a color eurp shadow mapping system does this behind the scenes before rendering color it switches the camera to match the perspective of the main light then it utilizes another shader pass the shadow caster pass to draw distance for each pixel we don't want these distances to draw to the screen though urp hijacks the presentation stage and directs it to draw to a special texture called a render target this specific render target containing distances from a light is called a shadow map hence the algorithm name to calculate if a fragment is in shadow we need our distance from the light and the distance stored in the shadow map to sample the shadow map we need to calculate the shadow map uv known as a shadow cord corresponding to the fragments position european again comes through with a function to convert world space position to a shadow chord urp will deal with comparing distances and sampling the shadow map if we just set the shadow chord in the input data struct in the fragment function of mylet forwardpass.hlsl go ahead and do that similarly to specular lighting urp toggles shadows on and off with a keyword called main light shadows however what if i'm making a dark scene with no main light in that situation i'd like to turn off shadows but i don't want to create a whole new shader with only this keyword undefined luckily unity has a system of shader variants for exactly this case using this hash pragma multi-compile command we can have unity compile a version of the shader with and without main light shadows enabled these two versions are called variants of our shader but more specifically variants of the forward lit pass multi-compile can also take a whole list of keywords in which case it will create multiple variants one with each individual keyword enabled by adding the single underscore it will also compile a variant with none of the keywords enabled conveniently materials will automatically choose the correct variant for the situation at hand if you want to use a variant with main light shadows enabled simply call enable keyword main light shadows on the material in c sharp disable keyword will undefine the keyword europe does this automatically if it detects a directional light in the scene create an object with a default lit material and move it between your mylit object and the light if you don't see shadows now turn off cascades and soft shadows on your europe settings asset it would be nice to support both of those options for better quality shadows though we've been talking like the main light has a position but since it models the sun it actually is infinitely far away from everything in the scene from the shader's perspective anyway this makes it difficult to create a shadow map containing the entire scene while keeping enough detail for good quality unity tries to balance this with something called cascades where it renders multiple shadow maps each containing a larger slice of the scene then it samples the one with the most detail at any position since the shadow map has square pixels you sometimes see their jagged edges manifest on surfaces soft shadows help eliminate these artifacts by sampling the shadow map a few different times around the given shadow chord it will then average the samples which effectively blurs the shadow map a bit we don't really need to worry about the details of either system though we only need to enable two keywords and urp will take care of the rest add more multi-compile commands for these new keywords with multiple multi-compile commands unity will permute each and create a variant for every possible combination of keywords we've barely started but that's already six variants each does take time to compile so it's worth it to keep this number low with that in mind unity 2021 tweaked the cascade system a little in 2020 you have to enable keywords for both the main light shadows and cascades but in 2021 enabling cascades implies that main light shadows are enabled we can handle both cases using a hash shift block and reduce the variant count in 2021 also notice the underscore fragment suffix in the soft shadows pragma we can save a little bit more compile time by signaling that the shadow soft keyword is only used in the fragment stage unity will have the variance created by this multicompile command share a vertex function let's test things out be sure to adjust shadow cascades and enable soft shadows to see all your shader variants at work you can see urp dynamically compiling shader variants when your shader momentarily flickers magenta now seems like a good time to introduce a powerful debugging tool the frame debugger find it in the window dialog under analysis make sure that your game view is visible enable it using this button in the top left it will also automatically pause when in play mode this useful window tells you all kinds of information about how unity renders your scene it renders objects in the order that they appear in the menu you can see unity creating the shadow map before rendering lit passes you can even check out how the shadow map looks the frame debugger also tells you which shader variant is currently active for any object navigate to the draw opaque objects drop down and then find your sphere check the shader name it will be mylit you can see the current subjer and pass and under that a list of defined keywords determining the shader variant try disabling soft shadows cascades and the main light game object to see how that influences things the window doesn't really do a good job of keeping the same object selected and you'll have to find your object again as you toggle things on and off there will also be a slight difference depending on your unity version so keep all that in mind you may have tried to apply the mylet shader to your shadow caster sphere and notice that it no longer casts shadows that's because casting and receiving shadows are completely different processes in 3d rendering and we haven't dealt with casting yet i mentioned erp creates a shadow map texture using another shader pass called the shadowcaster pass all we have to do to add shadow casting to my lit is write this pass remember passes our shader subdivisions with their own vertex and fragment functions facets can also have their own multi-compile commands and shader variants each shader pass has a specific job given to it by urp the forward lit pass calculates final pixel color while the shadowcaster pass calculates data for the shadow map honestly this is all much simpler in practice than it sounds urp takes care of calling the correct paths at the correct time and routing the output colors to the correct target these abstract passes are kind of hard to visualize so i find it's best to just start writing begin by adding another pass block to the mylit.shader file duplicate the forward lit pass changing the name and light mode to shadow caster there will be no lighting here delete the specular color define and the shader variant pragmas for organization i like to write each pass in its own hlsl file change the hash include line to refer to mylet shadowcasterpass.hlsl then create a new hlsl file called mylit shadowcasterpass.hlsl open that and start by defining the data structs in attributes we'll only need position while interpolators only needs the clip space position in the vertex function call the urp function to convert position to clip space and then set it in the output structure and finally return it in the fragment function simply return zero but wait why did we just return zero isn't the shadow map supposed to encode distance from the light camera it does but the renderer handles this automatically see clip space positions encode something called depth which is related to distance from the camera when interpolating the rasterizer stores the depth of each fragment in a data structure called the depth buffer unity utilizes the depth buffer to help reduce something called overdraw overdraw occurs when two or more fragments with the same pixel position are rendered during the same frame when everything is opaque only the closer fragment is ultimately displayed any other fragments are discarded leading to wasted work the rasterizer can avoid calling a fragment function if its depth is greater than the stored value in the depth buffer urp reuses the depth buffer resulting from the shadowcaster pass as the shadow map but most of the passes have a depth buffer of their own as well our mylit object should now cast shadows but you'll see some really ugly artifacts these are traditionally called shadow acne these artifacts occur mostly on surfaces of a shadow casting object it's another consequence of every programmer's bane floating point errors in this case the shadow caster depth and the mesh depth are nearly equal so the system sometimes draws the shadow on top of the casting surface to fix this problem we need to apply a bias or an offset to the shadowcaster vertex positions when calculating clip space positions there's no rule that they must exactly match the mesh we can offset the positions away from the light and also along the mesh's normals both of these biases help prevent shadow acne the shadowcaster now needs normals so add a normal field to the attribute struct then add this function to calculate the offset clip space position it requires world space position and normal apply shadow bias from europe's library will read and apply the shadow bias settings it requires world space position and normal as well as a rendering light's direction urp also provides this in a global variable called light direction first we need to define it like a material property do that above the function and then pass it to apply shadow bias apply shadow bias returns a position in world space transform it to clip space using this other urp function transform world to each clip clip space has depth boundaries and if we accidentally overstep them when applying a bias the shadow could disappear or flicker the boundary to depth is defined by something called light near clip plane clamp the clip space z coordinate by the near plane value defined by this constant to make things more complicated certain graphics api reverse the clip space z axis thankfully your p provides another boolean constant to tell us if the boundary is a minimum or a maximum use a hash if statement to handle both cases and then return the final clip space position calculate the world space position and normal using urp's conversion functions then call your custom shadowcaster clip space function back in the scene editor things might look immediately better if not edit the shadow bias settings and the near clip plane on the main light component you can also set global bias settings on the urp settings asset before wrapping up we can optimize the shadow caster a little bit by adding some metadata to the pass block since the shadowcaster only uses the depth buffer we can basically turn off color using the color mask command this color mask 0 command does just that basically telling the renderer to write no color by default color mask is set to rgba which is what we want in the forward lit pass for example lighting and shadows are some of the most fun aspects of shaders and we'll be revisiting them much more throughout the series after this tutorial you'll have a good ground work to continue with more complicated features later on we've got point lights spotlights make lights emission screen space shadows and a whole lot more to get to however in the next tutorial i'll pivot to transparency we'll learn how to create transparent shaders and how to handle their idiosyncrasies i'll also introduce some of europe's powerful optimization tools for your reference here are the final versions of all the shader files in this tutorial you can also view them on github from a link in the video description please stay tuned subscribe and press the bell to be notified when my next video tutorial goes live if you enjoyed this one consider liking the video as well it really helps out with the youtube algorithm having any issues or questions feel free to leave a comment or contact me on any of my social media i read everything and i'll reply when i get a chance i want to take another moment to thank all of my patrons for helping make this video possible and give a big shout out to my next gen patron creepydoobydoo thank you all so much it really does mean a lot and if you want to download example shaders from this tutorial and all of my other ones consider joining my patreon too thanks so much for watching and make games you
Info
Channel: Ned Makes Games
Views: 12,114
Rating: undefined out of 5
Keywords: gamedev, game development, development, unity, unity3d, madewithunity, programming, game design, csharp, nedmakesgames, nedmakesgames dev log, indiedev, indie game, dev log, shaders, 3d modeling, blender, tutorial, walkthrough, shader, universal render pipeline, urp, unitytip, unitytips, shaderlab, hlsl, beginner, starting, shader graph, hdrp, graphics, graphics programming, tech, tech art, lighting, shadows, texture, textures, interpolation, struct, swizzling, macros, material, materials, model, modeling, uvs, unreal
Id: 1bm0McKAh9E
Channel Id: undefined
Length: 27min 27sec (1647 seconds)
Published: Mon Aug 15 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.