Shader Optimization – True Instruction Cost, Performance Tips // Tutorial

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
today's topic uh optimizing materials from the very basic theory to some details within android engine instead of some concrete examples i would rather stick to some synthetic ones take a look tim jones has something called shader playground so the shader playground is a collection of typical popular shader compilers from hlsl or glsl to assembly so it doesn't serve a purpose in itself it's for diving into the shaders checking out what our code is converted to so as i said at the end of the previous stream assembly language is easier than you think okay this is the most basic shader if you are used to using nodes in unreal you may find it scary but it's not in general here in the ps input we have vertex attributes this one has only vertex color and here is the main shader program which returns the final color in unreal you are probably used to the fact that we have multiple outputs like base color roughness normal here it's more like an unlit shader with only emissive color on the output because this is how actually shaders behave but in unreal they are packed you know they are wrapped and then the output is written to g buffer like the base color the roughness and so on all the attributes are written to full screen buffers and then there is another shader invisible for you the user uh or the game developer which is the lighting shader that's run after this but these are all details of a very complicated pipeline and actually we don't care if you want to measure performance of the shader we don't care how much it takes to render lighting we don't care what's happening before and after because we should only care about what we can influence which is the actual content of the material so it doesn't matter if we learn what's heavy and what's fast on the synthetic example this knowledge will be equally you know appliable to final materials jeffrey says i missed your previous stream will i need some stuff from the previous stream to understand this one no don't worry i only show this at the end kind of a bonus and i will start totally from scratch so yeah you're good to go so this thing shader compiler by default it uses microsoft's compiler which outputs something like this and this is a modern assembly of directx 12 probably or late 11. it's very you know it's very efficient i think it's based on c lang if i'm correct very good assembly for the computer not so much for us humans as you can see there's no team there output weird symbols that then they they point to some other places in the code it's all good if you are a machine very efficient but actually i prefer prefer the old style assembly so we can switch either to fxc the previous compiler or to my favorite radeon gpu analyzer let's use this one as you would be able to see the assembly will be very different like this this is all um and these are the actual commands for the gpu so it doesn't matter yet what it is i will explain everything so don't worry the most important thing is that we can switch from this assembly to isa breakdown which is almost the same [Music] but in this breakdown we have the cycles column and the cycles are the actual cost in gpu cycles for this program for this pixel and if this says 4 it's always for no matter what of course it may depend on the hardware so here asic is the actual graphics card that you're aiming for here is not explained but in general the biggest number gfx 1030 are the newest radians the rdna a2 ones so currently newest s of 2021 jfx 900 is the previous generation but still modern still newer than ps4 and xbox one architecture and now where to learn the keywords there is a great article how to read shader assembly just like that okay and aprico by costas begins like that when i started graphics programming this is a quote from an article shading languages like hsl and glse were not yet popular in game development and shaders were developed straight in assembly when hlsi was introduced i remember us trying for fun to beat the compiler by producing shorter and more compact assembly code by hand this is an interesting thing compilers by default optimize everything they can and shaders are such a simple environment such predictable environment with not too many instructions to be honest that compilers can they can do a really really great job in optimizing so first before i explain what it is let me output something different or maybe i will explain that if you have let's name it because this is what it is the vertex color okay and on the output you have float4 which is rgba command compile yeah then you have move move so move is assigning something to a variable on to and target register interp is interpolating the vertex attributes to the pixels because pixels cover you know some triangles and the vertex color is linearly interpolated smoothly between vertices so this is what that is doing interpolating value from the vertex and the pixel then it's converting cvt from 16-bit to 32-bit float f if you see f in assembly it's usually float if you see i it's integer and then x export and end program this is it so this is doing nothing else than just taking vertex color interpolating it between vertices putting it on the screen the end okay now let's try this input vertex color times and if you wonder if this is useful in unreal yes it totally is and i hope you will see later why okay then you break down what has changed still interp still convert but in the middle there is new thing there's small short of multiply multiply some 32-bit plot numbers and the multiplication is happening four times because this is float four four components of the vector each of them is multiplied by a constant that you can see here two two two and each of these multiplications will take four cycles like so so now let's try what will happen if we by the way i may also reduce this from float four just to make the assembly shorter let's say that this is not a vertex color pair vertex it would be some mask pair vertex okay and my output will also be float just for simplicity this won't compile of course because vertex color is not here return input mask timestamp let's go okay cool doesn't this shorter more readable i hope so it's interpolating just this attribute and what and it's adding why ah because it's times two so it's the same thing plus the same thing so i think here you can already see some optimization this compiler is trying uh okay compiler stop cheating because now i will use return sign of mouse alright as you can see sine is an actual instruction on the gpu but the input has to be altered a little by this multiplied by this number first then it's given to the sign instruction so in total sign takes 20 cycles on the gpu okay we have let's open andrea's material and i will show you what andrea does with this sign we have a test material i will change it to unlit and by the way there is some overhead from having a material because as i said it has to do some instructions first some instructions after because andre has a complicated you know detailed pipeline of rendering it's not not just straight to the screen okay uh so first we need we need to test what's the basic cost so cost without anything so i will do an emissive color it can be a float i think uh one that's it oh yanazkin says that this magic number in the compiler is one by two pi 1 by 2 pi this is it yeah so i think that the sign instruction the gpu it doesn't work with the typical period of 2 pi so it has to scale the input to use that and give the correct result right between skin probably this like this is like that okay so back to unreal we have material that does nothing and outputs one now let me show the stats and andrea says that the base pass shader without light map takes 29 instructions okay 29 instructions now the important thing is that if we test optimization we always do that on live parameters like this my pattern otherwise you risk that the compiler will flatten the math because constants like for example this plus this and if this is a constant and this is a constant one plus two then the compiler already knows when compiling the shader that this is always free and this always equals to 3. so what will happen is it will just collapse that to a constant 3 in the background okay so no matter that you placed add here if you add constants it will just replace them with the result i can prove that here so if we return no sign of input mask but if we return one plus two plus three and let's wait for it mhm here so as you can see there are no calculations just the export export of this which is probably six in hexadecimal okay so the same will happen here uh currently it's 29 instructions right but if i add and then multiply but everything by a constant let's say three plus one times two and to the output you'll be able to see that it's still 28 instructions the compiled thing has still 28 instructions because this doesn't matter the return value is known compile time constant folding optimization exactly constant folding are using the custom node in unreal prevents constant folding and may use significantly more instructions than an equivalent version done with built-in nodes let's try i do wait let's try how it counts that okay so custom is a node where you write hlsl code so code is here and let's write and it will be x plus 2 times 4 31 instructions so as you can see using a dynamic parameter already adds more instructions now if i use that here instead 29 instructions so yeah so yeah that's true that using a custom node produced more instructions actually it didn't fold the constant here okay so anyway back to this we had my param and uh now my power if i multiply that with another param let's say or no if i multiply that by some constant multiply let's say and add okay multiply and add multiply by three and add one cool and let's try that 29 instructions now check this out uh if i use a sine node that we already know is five times more expensive than multiplication 29 instructions so it's not that unreal is so optimal no it's that this is actually fake 29 instructions according to unreal are produced by that code compared to this code this can't be true sign is much more expensive as an operation now take a look sine and then cosine so it should add 40 gpu cycles 29 instructions so take a look here if i have my input mask let's say okay this is mask and i will also have x card 0 for example i don't know uv okay and it will be mask times input uv x okay and now the breakdown so you can see there is a multiplication here four cycles right this is it this doesn't matter this doesn't matter four cycles multiplication now instead i will do return line of input mask times input uvx and down so there are two multiplications first by the u v x here four cycles by the constant that pionos can mention this one by two pi four cycles and then sine 16 cycles this is how what changes instead of uh one multiplication four cycles we have 24 cycles by adding the sign now if i add cosine to this thing you'll be able to see that we will have at least another 16 and probably even 20 cycles more okay cool so breakdown there's mal 4 cosine 16 there are another mile here 4 and 4 and sine 16 so this is at least 40 cycles more than just the multiplication for t and unreal says that we it added just two instructions sine and cosine one and two this is not true so this instructions has a huge problem hidden in itself it doesn't show you the gpu cycles it shows you the basic i don't know hlsl instructions but instructions differ a lot in the cost one compared to the other multiplication is the cheapest one for cycles sine and cosine are 20 cycles now instead of this sign let's use dungeons and so yeah tangents is uh made out of sine and cosine and reciprocal reciprocal is 1 by x then multiplication so four cycles 16 16 16 then four yeah so this is a heavy heavy instruction now if i used tangents here and compile 29 instructions so yeah basically unreal's instruction count is how to put it lightly an approximation but often it's total nonsense yeah so if you are in doubt try to convert your code to hrsl in shader playground yeah one more thing if you have something like let's say data temporal aaa and this is the green node the most basic one the blue node so it's a material function while for many of you it may be obvious remember guys that you have to look inside double click that and you may be surprised what you find there this material this material function determine aa contains even more functions and when when thinking about the cost you have to basically sum everything okay so it has some constant add another add divides custom look inside there is a modulo cast to uint addition of two another cast to u n multiplication multiply by five so there's a lot happening here in a simple single node so be careful even simpler ones like cheap contrast or chip something oh rotate about word access cheap very cheap right the name implies that looks like this and each of these nodes are actually not so not so cheap because there's cosine sine sine cosine three times okay of course it depends because in unreal uh if you connect only one of the outputs if you connect only this then the rest won't count let me show that so i have my param which is the rotation amount and the x-axis okay let's apply 37 instructions now what would happen if i choose the max of two max is a very cheap instruction four cycles by the way if i choose the max of these two it's immediately 33 instructions it's not because of the max is because of the fact that unconnected pins don't count they are cutaway so yeah connecting things as them to the calculation if they end up in the output if they don't like this they immediately stop counting see i connect that 38 i disconnect that 28. so 28 in this example is our baseline now about this optimization take a look if i have a variable note let's say result equals tangents of input mask but then i don't output this just return to then the compiler will optimize that away you'll be able to see that in a moment so breakdown is super short it does nothing it just exports to move so assignment to the register and then exporting this register job done so yeah as you can see compiler does a great great job okay now take a look if i let's say if i let's say do something in an unoptimal manner let's come up with something stupid um let's say we have float okay first result equals tangents in input mask as it was then result will be multiplied by a fractional of uv of input uvx that's good let's return that you will see like the proper output what's happening here tan is done through sine cosine reciprocal which is 1 by x 16 16 and 16 cycles so quite expensive operations then fract which is frag modulo by one and it's four cycles multiplication and that's it cool now what if i did something stupid and if i multiply the result in the end by let's say uh zero like this as you can see the assembly became much much shorter it just exports zero okay because it knows the compiler knows that this will always end up as a zero so in this how to read the shader assembly it's written that they tried to beat the compiler for fun by producing shorter and more compact assembly code but the thing is that modern compilers in the current iteration are so good it's really hard to beat them without doing a meaningful change in the algorithm itself okay so if you can come up with some optimization that actually you know cuts some corners that uses a different approach then sure it's worth trying but if you're just thinking you're clever and you're trying to change the order of some operations or something they don't don't bother compiler is great and this every compiler actually at this point so sony xbox and amd don't try beating it in simple math okay so if you let's say multiply the result by 3 plus 2 you will see that it the ad would be cut out so in the isa breakdown there is more multiply by this a constant constant of 5 but there is no add anywhere this was just converted to five that they can prove by replacing that the same number actually just five so compilers are yeah the commit you are right multiplying by zero is the ultimate optimization it results in the shortest assembly and the least memory usage okay just kidding uh so this this thing now um there is another concept in compilers about which we we should talk now is the changing the orders of the operations if it makes sense from the optimization perspective it does so automatically when the changing the order doesn't alter the result let's say we do load and this will be let's say apply and apply tangos and there will be a floating value of this is a function in hsl and it's similar to making a material function in unreal okay so this will return return uh 10 of file like this okay applies tangents to input mask result 2 we apply tangents to input mask again and this again the same thing and the actual the result will be result 0 that's result 1 plus the result who so we have a clear order of operations right and buy dungeons i play dungeons again i play it again then two additions right not quite take a look what's happening is that it's just the tangents once these four instructions and then it multiplies the result by three constant three okay so it found out what we're trying to do it spot for a while and came to a conclusion that we're stupid as humans so it used its computer magic to convert this into basically 4 16 16 16 and 3 times 4. when i disable that and if i just do result 0 and then return result 0 times 3 it will be the same assembly probably yep as you can see exactly the same assembly so 16 16 16 or 4 4 and multiplication by the constant three exactly the same code that it produced automatically before out of this knowing that we humans have limitations we are stupid and it used its computer magic to find out the better way so we didn't beat the compiler by doing this it resulted in the same output now there's another thing when it comes to to the order of things there is a node that you might may remember texture sample you have some texture sample param like this let's call it my text my text uh parallel to d no texture object parameter my text this is a texture file basically this one for example you put it into the texture you apply let's say uvs multiplied by hail tail or tiling in that regard you put it into uvs and you output the emissive color from this one super cool it says 31 instructions no oh the texture sample is one of the heaviest instructions in terms of cycles not the instruction itself is cheap but the fact that it has to wait for the texture to come from the memory the thing is the thing is that if you if you have gpu let's say here you have your trader car like so okay you have your shader core uh near the shader core there is the fastest memory uh inside that there is inside the shader there is a one super super fast cache and light d outwards should be l2 l3 so the slower memory slower but still extremely fast faster than ram okay faster than gddr5 and somewhere there because we have many of these scores working on many pixels at the same time somewhere there you have actual maybe not this size but actually you know huge uh vram gddr5 for example okay the thing is that this memory is very limited this one but the access is super fast super fast but this is like some kilobytes maximum uh so these this memory and register memory and so on is used for the immediate variable so you have a limited amount of them so per shader car you have typically on modern hardware like i don't know 96 registers or something so let's say you can keep 96 floats uh in the immediate memory but if you want to read something so huge as a texture then it won't fit here you have to go all the way all the way to rum let's say there is some texture it's i don't it doesn't look like that of course i'm just you know making this up but in general there is some texture and you want to sample this place for example you're interested in this uvs this is a whole texture and you want this okay so it gets you this sample it goes back all the way actually to the cache and for a moment for some short time it will keep this in the cache this pixar and its immediate neighbors because it if you sample this place with your current uvs there is huge chance that the next pixel will have similar uvs so it's worth it to take more than you just then just requested just in case that the next shader core actually wants hey i want the same thing just this pixel the neighbor cool so small part of the texture can be held in cache but it will only make the next operations faster in the meantime this this thing can take like 200 cycles or so even if it were less even if it were 100 this is still huge this is like you know doing sine five times this is long and this will be it will get even longer depending on you know the texture content and so on so the journey to vram and back takes a long time okay you want to avoid that but the compilers are clever because they are computers and they know magic so let me show you what they will do if we have such example they can do nothing because the sample goes at the end but what if what if we had some calculations independent of this texture if we had for example i don't know some param that would be i don't know layer layer hive okay if player from the ground and we want to apply let's say sign on that we want to i don't know take it also a close sign of that we want to check which one is bigger than the other and later we want to i don't know thank you vs i'm making things up completely just you know to make some heavy math take uvs and multiply that you may think this is a lot of operations or i don't know even worse take tangents of the uvs then okay and multiply that and put it add it let's say to the texture result like so um like so this is completely stupid but it doesn't matter so we have texture which takes a lot of cycles to to be delivered from the vram we have heavy calculations let's comment this by naming it heavy calculations okay and this will be heavy texture or something so you might think that this heavy thing plus this heavy thing will make it twice as heavy shader it's not true i will show you why in a moment some of that asks if uh having 12 texture samples in material is not a very good idea it's not you're right it's absolutely absolutely not but there's one trick now having fewer texture samples but a lot of math can result in some free math muffler free how's it possible take a look we are waiting for a texture okay it will take hundreds of cycles to go back to us so the gpu is idling it's waiting so why not calculate the texture independent stuff in the meantime okay this shadows this math because it will take so long that we have cycles for free while we are waiting so we can calculate all this and don't even see a difference this compared to this will probably take exactly the same amount of time or very similar take a look at the assembly load texture sample will be texture we need to make a texture so uniform texture to d um my text and this there will be also a uniform uniform is a parameter in hso uh yeah the result should be texture sampled like this now about this uvs let's make our own uvs uh like we have in unreal our uvs are marked multiplied by tiling okay hiding and now let's remove that and now there will be float uv equal to input uv times tiling okay time styling and here let's see okay so what we have we have a multiplication here by the way buffer load is loading a parameter or something else some other constant this the same for entire shader that's why it says it's scalar memory so things that are the same for the entire material but still need to be calculated are scalar memory things that are dynamic per pixel or per vertex are vector memory the name doesn't you know scalar or vector it doesn't have anything to do with float or float4 it's just the name for the type of memory scalar is operations for the entire shader vector pair specific pixel so yeah we wait for these parameters to be got to be retrieved from the memory then on the tiling well it's already let's say downloaded from the memory it's multiplied by with the uv here uh and assigned to a register then there is image sample so reading a texture under a given uvs here my text sample is the same as in unreal texture sample okay and then the result is put like move is assigned to the output and export it this is it uh so now what would happen in theory what you would expect to happen is if you do some math after this texture it will be applied later but as i said it doesn't work like that it will it's much more clever we will add something called heavy calculations input oh and will be there will be a function float heavy calculations that takes ps input input and returns some heavy result in our case it was player 5 sine cosine max tangence of uv heavy calculations max cause ah unifrom thanks some of that okay fantastic [Music] there is a weight counter here but it probably waits for this uh uniforms and weight counter basically says that at this point you have to stop because the next instructions depend on what was being loaded so you can do whatever you want here but here you have to stop because you will need that later so now there is uh where is it yeah before image sample there has to be a multiplication because image sample depends on the multiplied uvs so we have to interpolate so get uvs from the vertex to pixel interpolate uvs here this one then we have to multiply by tiding and only then we can sample the image but it doesn't immediately sample it just sends an instruction to sample and then continues with the flow so while this is going all the way to the vram memory is doing next things it multiplies uh applies cosine here applies sine here it does that two times uh probably oh yeah because of the tongans then it takes the maximum of these things this cosine sine then it multiplies here by the tongans result and only then after all this is made all these calculations it here it has the weight counter so only here it resolves the it really waits for the result of the texture so even if the heavy calculations appear to be done after the sampling they actually happened during the sampling and only then there is a weight counter and addition here and it's put to to the result move export okay so this is it basically something a texture can shadow a lot of calculations making them basically free often depending on how heavy the texture is and how lucky you are with the cash as you can see if the neighboring pixels were already sampled then you may get this also cheaply but if your uvs are chaotic then you paid heavy cost which is another thing by the way that look at this yeah if your uvs are kinda consistent let's say uh i know you have an object right and then you have your your x of uv is going here you have your e y going here and basically if this is a nice smooth gradient like a typical uv that is good because one pixel samples one one pixel samples this place another pixel here another here this is all coherent okay in terms of standard uvs but if you apply some weird effect to your uvs let me show you then this would be much more expensive what i mean is that this is how standard uvs look like rock oh why is it so slow come on real time can i disable real time i should be able to yeah it became nearly unresponsive okay standard uvs how do they look like come on unreal they look like this right that there is uh unlit yeah x is going one way y is going the other and the yellow is basically one one here zero zero one one now these are nice uvs because the neighboring pixels somewhere from a very similar place so the texture can be placed in a cache so when asking for one value after under some uvs the pixel already gets the neighbors and puts them in cash to avoid unnecessary waiting for the neighbors however if you have a noise for example vector noise vector noise uh you take a component mask rng and you add this to the uvs like so because you need some fancy effect okay if you do that and the uvs look like this maybe not as noisy can be weaker yeah let's say if you add some noise to your uvs okay this may be a really nice effect but what it actually will do is that you can no longer rely on the neighborhood because if one pixel samples here but another due to the noise samples here another here another here and so on then the cache doesn't apply it doesn't apply because this one we take these neighbors this one will try to use the cache but oh this is out of out of the cache so i need to sample again so noisy uvs or incoherent or many splits of uvs will make you pay a higher cost one sampling okay so the gummy asks if it's better to do glitches on texture rather than on uvs often i'd say yes though it depends because sometimes a glitch on the texture will require another texture to sample so sometimes it's okay to add some subtle uh uv glitches or sometimes you even have to pay the cost because if your effect is dependent on it then there's nothing you can do right but in general yeah if you can avoid very noisy incoherent uvs then don't do that also there's another thing textures have mipmaps that's because let's say this is an object of this size on the screen but if you are far away here then your uvs are different because your uvs suddenly will be like i'm something this part of the let's say if this is a texture okay this is a texture thanks then if you have neighboring pixels on the screen usually gpus work in groups like 16 by 16 pixels or something 8x8 so if this is my group here this is a whole different story than my pixels group of the same size here you know this object is further away so suddenly while this was doing a a nice use of the cache because this was something here here here and here for example then immediately this far away object here uh this one it will sample like here here here and here so completely you know it doesn't work for the cache so this is why mipmaps were invented this is why blurry textures are good mipmaps are versions of the same texture but smaller right you probably know that so smaller smaller smaller smaller until four pixels by four in that case it knows that the uvs change so quickly that it doesn't have to use this you can use that with the same result because this is a resolution enough for the size on the screen so this object will use meep 0 while this object will use mip 2. okay so this is what meeps are for and if you disable mipmaps because of blurriness don't do that don't do that this is very very good for the cache this may be one of the reasons that modern games can apply so many textures because you have i know normally you would have like billions of samples to take per frame like impossible but thanks to cash and mipmaps usually this can be avoided in reality we pay like one tenth of the cost or even less okay because if uvs are coherent and mipmaps are there then usually you sample from a very small texture in practice and you utilize the cache in shader course okay do you have some questions so far and by the way about resources this is a hard read but very much recommended fabian gizen he has he has a series called graphics pipeline a trip for graphics pipeline basically it's a trip through let's say modern by the time it's 10 years old but still quite modern a graphics pipeline so if you stumble across some old opengl unchained resource then this is a very good refresher of how things work let's say now because nothing has not so many things have changed actually ray tracing is a new thing but it doesn't change this thing rasterization is very mature at this point so very much recommended uh rip or gpu pipeline legomine asks what if you have few samplers to one texture so what if i put this texture on the left this texture on the right and then sample few samples but the same texture the thing is it doesn't save much it doesn't save much because what's costly is sampling so texture okay at in a bigger atlas sometimes okay because you save on the number of files so streaming but you still paint the cost of something which is the worst in the shader itself so combining textures only helps outside of the shader but in the shader itself it doesn't save much but you can do another thing which is actually very useful you can reduce aim to reduce the number of samples okay so if you combine let's say this is a color example so it has to stay like that but what if you actually can with what you actually did only one sample so in that regard there is a plaster and there is some shape if you are okay with the fact that the shape will be one color or grayscale you can pack them into rgb do i have designer here i do okay there's substance designer come on new substance yeah now let's say we have some black and white spots and let's say we have some crystal cool now if i'm okay that i will use these two with the same uvs and i'm okay that they're grayscale then what i can do is to merge rgba i'm packing this into red and packing this into blue green into blue i can pack even some other noise like this with different scale and the result is this this and in the red channel it has my first texture in the green channel it has my second texture and in blue it has the third one so let me export this thing and sample it in unreal so let's save that now let's save that as bitmap test material let's jump to this one okay compression will be masks cool and you can see red green and blue okay i save that and now i can get rid of this because i will only sample once and this will be this masks texture sampler type masks cool and yeah come on dogs uh and now i can make a gradient oh sorry uh content drawer let's make a gradient asset i should have imported this here what what's that there is some gradient acid right there was something materials um how was that code guys this curve atlas thing carefully in your color okay was that it yes awesome okay can i select this as a color or i have to use this weird things ah yeah choose color let's say yellowish scent color to white all right choose color darker nice little gradient very cool now this is one curve now i need another curve now in the atlas i add another curve they are here and the hive automatic and jeffy asks if i'm going to use that to colorize grayscale images yes absolutely let me prototype that in them but this is what i'm going to do if i have this on the output i could do a gradient map like this if my gradient contains colors like so then you can see that it's colorized based on this gradient so grayscale is used to pick the value from 0 to 1 on the gradient from the plane as a color this is what i'm going to do to use grayscale masks as a base color because i will use grayscale masks plus gradient so the same in unreal i have my grayscale masks here um these packed here and i will use gradient popping where is it okay so curve time from 0 to 1 will be my red from this let me preview as you can see what we have in decider that the gradient this one colorizes this okay i need to modify it a little like so now it's better yeah super retro look by the way so this is one gradient and there's there will be gradient one maybe one or another one for the green one if i use the same grayscale oh hey wait a second it has to be square or it seems like it has to be square that's weird okay anyway um yeah so the based on which gradient i'm using i'm getting a different color right so now this i will use for red this i will use for green which is another pattern and now i can verb between the results having just one sample so one texture one sample instead of two textures two samples and as you can see stop breathing yay i have my mixed result just like this so see you
Info
Channel: Tech Art Aid
Views: 29,299
Rating: undefined out of 5
Keywords: tech art, ue4, ue5, unreal engine, tutorial
Id: y0QASid1v8w
Channel Id: undefined
Length: 63min 34sec (3814 seconds)
Published: Sun Jul 11 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.