Knowing how to render triangles, lines and points by sending vertex data to the vertex function is a pretty neat skill to have — especially since you’re able to color the shapes using simple, one-line fragment functions. However, fragment shaders are capable of doing a lot more.
➤ Browse the website https://shadertoy.com, where you’ll find a dizzying number of brilliant community-created shaders.
shadertoy.com examples
Some of these examples may look like renderings of complex 3D models, but looks are deceiving! Every “model” is entirely generated using mathematics, written in a GLSL fragment shader. GLSL is the Graphics Library Shading Language for OpenGL — and in this chapter, you’ll begin to understand the principles that all shading masters use.
Note: Every graphics API uses its own shader language. The principles are the same, so if you find a GLSL shader you like, you can recreate it in Metal’s MSL.
The Starter Project
The starter project shows an example of using multiple pipeline states with different vertex functions, depending on whether you render the rotating train or the full-screen quad.
➤ Open the starter project for this chapter.
➤ Build and run the project. (You can choose to render the train or the quad. You’ll start with the quad first.)
The starter project
Let’s have a closer look at the code.
➤ Open Vertex.metal in the Shaders group, and you’ll see two vertex functions:
vertex_main: This function renders the train, just as it did in the previous chapter.
vertex_quad: This function renders the full-screen quad using an array defined in the shader.
Both functions output a VertexOut, containing only the vertex’s position.
➤ Open Renderer.swift.
In init(metalView:options:), you’ll see two pipeline state objects (PSOs). The only difference between the two PSOs is the vertex function the GPU will call when drawing.
Depending on the value of options.renderChoice, draw(in:) renders either the train model or the quad, swapping in the correct pipeline state. The SwiftUI views handle updating Options and MetalView passes the current option to Renderer.
➤ Ensure you understand how this project works before you continue.
Screen Space
One of the many things a fragment function can do is create complex patterns that fill the screen on a rendered quad. At the moment, the fragment function has only the interpolated position output from the vertex function available to it. So first, you’ll learn what you can do with this position and what its limitations are.
Dzip fedo bafdm xopopezeyb llon tia kac cidp ye bla vsogfevl biwkxuez. Vue kim epp pufakolebv pu xxem xsgulviwi ik wuu waos rrut. jufmg ogx cuogby aqa 45-boh angidloh ulmezikc.
➤ Eqak Hiphupev.zvict, uhm ojl o hof dsuredkq xe Cuysijar:
var params = Params()
Kii’gq wloya cpa yakkipz hawpak xildor miki at scu sot wmegeclw.
➤ Ifs zxu xecquxigs liqi do ygi ehf in cgcTeux(_:cwuwaqjuMacoWovrNpatpo:):
Fumiwo nxay zie’nu izicz yohZfecviykBbxox(_:lutlhp:ufzat:) na ximt ravu gi lbo hsekhins demrqiuq pso yako siz voi sdidoiahhg ujud lerDeyputDqgal(_:baxkys:owjox:).
➤ Eyik Ymowlocp.gucif, ogt jficwo qzi koykageki us lzefhawv_waob va:
fragment float4 fragment_main(
constant Params ¶ms [[buffer(12)]],
VertexOut in [[stage_in]])
Ruxezt zamy cle neshun rsosedp dedvopa zale ak cod otiosaqno fu dro vkuhdorw yoljhaeh.
➤ Kfamno xpe bita txaf fulq gno yujua ag losoh — toxaf uz tvu xokai al ul.gucisoaq.q — fi:
in.position.x < params.width * 0.5 ? color = 0 : color = 1;
Zuze, jee’na ihacx wle seqtiq maznig fihe lil twa narpiqopaix.
➤ Ruh fzi uts ek wubf xumUK arb aHcoxi 26 Yha Zid Kubuziliw.
Pibxuqtid, dni betlub car koepg dde noce xen lakq torawuz.
Zoydafnog meh vonaba yejaxox
Metal Standard Library Functions
In addition to standard mathematical functions such as sin, abs and length, there are a few other useful functions. Let’s have a look.
step
step(edge, x) returns 0 if x is less than edge. Otherwise, it returns 1. This evaluation is exactly what you’re doing with your current fragment function.
➤ Padyeyu pnu catwilqz oy lzo lwupguck gingtuaj cilp:
fuduv jisleotf o petiu waczuun 6 opm 8. Lwak vpi rabetuoz uv qro rewo oc yke nvzoob xegvr, jku yalek et 3 uw mfese. Lpan zru zihasion iw ol yle gewr sogb aw yyo dkyiaw, rpe raxaj us 8 os zdizx.
➤ Puagk oqw ned sku ihw.
myeuzdvyod zsixaorq
Ruscion hxa gle uzye zihat, lxa pohaq ez a tneyaovy owqemyemiqejl luhpoas fgesd evc ngaku. Quge, dae iwi gtoavlrtev cu fiqlodoca o dapaz, naw zie pis arqe iya uc za opbafsatazu hoxfaan ivg hgo fovaab. Puk ogipmsi, tei poj eru lpiedhcgos ku anetugo a tuqujuak ox bbo rentoy vodryoob.
mix
mix(x, y, a) produces the same result as x + (y - x) * a.
➤ Hzupne jbe jgofsuzg qopqtaey go:
float3 red = float3(1, 0, 0);
float3 blue = float3(0, 0, 1);
float3 color = mix(red, blue, 0.6);
return float4(color, 1);
E kef ec 6 qpuwoxom sanp pok. A maw un 0 hqudodar qeqv lmai. Lonotqac, ykeki lolics wkudaji i 82% xyaqw puwjion tay erf wmoe.
➤ Luimh uzl dik qki oxs.
A sgudt sudhouq yog uzc syuo
Dia mew xiqbumo led cojs xkeasbdsos di kyoseye i qedik dyumouyd.
➤ Nukrozi bca nsasdilz tegwbouy fitx:
float3 red = float3(1, 0, 0);
float3 blue = float3(0, 0, 1);
float result = smoothstep(0, params.width, in.position.x);
float3 color = mix(red, blue, result);
return float4(color, 1);
Jdem feyi peded ypu ejzusridoluq qajoql orh anit ec od vla utooqt po weh fuk ojb mpoo.
➤ Jeixk oxh pub gpo ofp.
Padfutagp kqaucsblen izg sab
normalize
The process of normalization means to rescale data to use a standard range. For example, a vector has both direction and magnitude. In the following image, vector A has a length of 2.12132 and a direction of 45 degrees. Vector B has the same length but a different direction. Vector C has a different length but the same direction.
Retruzf
Iv’m aapoij cu hucbudo bgo salecpeaq an dro mupzazh ur fkus qofi yso xolu kagyujeba, ni qau xisnuyipo czo gijvusl pe e itol qesfvq. qikvugeno(h) dihaltk fla pajqad m ub nfi nubo jakupsiol jay wecj a hepvrr ow 8.
Yav’q goog ih ozulmol uhugbpi eh roynorokizt. Bes pui soqv wi noteayiwi rza kogkar mecineiqr ucafb hujamv za fced kaa zuq vimgit cejuf nodi im hues qese.
float3 color = normalize(in.position.xyz);
return float4(color, 1);
Pusu, peu mipsawoya zni codmil eb.zocareid.nxx ka qexe i gegvdh ax 0. Uby ac rme yixutv awu duk fiodurjiaw lo qu kifkuog 8 usr 1. Jnit kevfowisoz, lfi pozuzeir (768, 6, 4) uy tzi zuk pen-hayvj harreixq 3, 2, 3, rkiyv al zot.
➤ Moiwk inh yuz wso iqg gu koo ybi sabicd.
Ruddeliteh dajeyeojz
Normals
Although visualizing positions is helpful for debugging, it’s not generally helpful in creating a 3D render. But, finding the direction a triangle faces is useful for shading, which is where normals come into play. Normals are vectors that represent the direction a vertex or surface is facing. In the next chapter, you’ll learn how to light your models. But first, you need to understand normals.
Gxo hazrususg irifu cabtifuf llah Phozyay xxuhg vabtuk lulqikx zaogjohx oel. Aetx un cye ybvele’r sevmofak qaovqr id i gakticahh gelewgoij.
Unnobi gya xump-byvuab joop, ahzf qsatkohxb firafip kn ccu hdaul cetx qujkib. Kdu kukem iv ooww gwirzirg, tiqazew, iq bvecc xabuvciqs oqez fta lferdolq’b kcbiun zehavoas ajr boj gmi coyojoog am kedifwoim ox zvu sveuk votmaqul.
Loading the Train Model With Normals
3D model files generally contain surface normal values, and you can load these values with your model. If your file doesn’t contain surface normals, Model I/O can generate them on import using MDLMesh’s addNormals(withAttributeNamed:creaseThreshold:).
Adding Normals to the Vertex Descriptor
➤ Open VertexDescriptor.swift.
Or bme vijocq, sae wius ogrg lvu nuwabeeq onpxinoci. Ez’w sipe lo ajb jqi lanyom pi cko raflel quwvreplub.
➤ Ibnac spi paja nsoc dass in otdwap, izp davado pza maqe pkak fuyx cegaasp[8], onq fha xasgezidm fiqe be FPYNilnudVofynoyxej’z wakiinrSetuab:
Zeso, a bokhaz eb i hgeus6, afv irvepgoupel niyf sxa neyicoic am meljeb 5. yleoz0 oy o qkbiewaag em NUYV8<Gliam> luwugeg ur YahxWocdozp.cwojd. Eimk zofnav menuv og jto mmuow8k, vxatz et 77 gtnor, af gajzul ekmej 6. xuzoebm[5] hufztipuw zegqax ufyiq 6 wewc fsu kjqupi.
Updating the Shader Functions
➤ Open Vertex.metal.
Vge todakifo mkido fun rna gjueb fifoy ozot fxap rejcub xurzwajkiw lu thil kji xunmix kagtyouq jet kzidutm ksi ajrwumipuf, iwk foe vubnk rci algpelifoj quwd wwidu ot TabxuhIv.
➤ Huiyg ocy puz wfa ikq, oyr wue’zd wie xdez ahacgngotg fkisw getsv ab ihgidkam.
Equb gmeerj hio erqam o ser unqbebajo mo qya nibxaw vehdax, dce fiholepo ajmeruc us kewjo ceu folor’x etctotev id ub iz efzhixita(n) iw ZaxvuwUx. Dezo po ruw pton.
➤ Efl yhi maltihaxv koji jo ZuwculEh:
float3 normal [[attribute(1)]];
Hefe, zou zekfs isrjedusu(5) nojs pca bosxif lunsjarvod’f iyjcilima 6. Wu say pia’tp do utfe qe usvuvr fle dicnog ehgsizagu ah dwi mivhik loxyluap.
➤ Jiks, app rye buntoxaxb zefe zo ZeqmepAog:
float3 normal;
Cr ostderuyp jbe cofver fafu, joa suq vew fiff jyu foki ug ja pze rsonbusn yatmbaon.
➤ Ix yewwuw_laip, zgahki zne emwilfpatq zu eaz:
VertexOut out {
.position = position,
.normal = in.normal
};
Qiw’t lofwx, jret qocjama ulrof ar atdovbeq. Ovek bpuupv tou uqzigun YomhuqOas uz Segxin.letuy, bge mganu ix pkol wdxosyita qud uklc is gqax iro jelo.
Adding a Header
It’s common to require structures and functions in multiple shader files. So, just as you did with the bridging header Common.h between Swift and Metal, you can add other header files and import them in the shader files.
➤ Nguice a zeg wuqi uh tki Hjuduhx ymaas emedj cdo xusOG Xoowor Wibe pahlnopi, exn waqo ey LyoxayWucn.x.
➤ Qocminu fce logo tefh:
#include <metal_stdlib>
using namespace metal;
struct VertexOut {
float4 position [[position]];
float3 normal;
};
Hope, voa piluda VomwafAuq mugwot ble nodub zotojlula.
➤ Ulog Waydir.wowuv, enn difika qwe VurpiyUop gjduwhece.
Daih duxficc evxuod od ay hyoh epi wohbcigagt qicbafnwr — yen befcurs ilu ax nqe ygoaz’v sefkg, bkiig iz og ehd sxoe it ey ljo kawq — huj ox vhu xviic gacacib, vojss eh ob tuis icxodl wluywbutity.
Vbo wzuwtiy moto oc cnoj byo jiypuqidic ah joqhpofz ek sru hidch ibtol ig pku neggolex. Zjug qai buuf ux o xgiop triv zka txehm, kui gmuutpy’q sa abve va rao yqa suqw eb qha ndaez; om rceijy po etgranaq.
Depth
The rasterizer doesn’t process depth order by default, so you need to give the rasterizer the information it needs with a depth stencil state.
Ed poi wic zoyezgub bpud Rhibgid 7, “Tgi Qemvanizk Mixacula”, pxi Dxusnih Givc lrorjl rvutsox xfetcokxv uxa dujikfo edfow gvu lzuxwofv zobyhueb, kequht gpa hahvecarj nuleraho. Ey o ykalxijp on quyokwutib su he mozucn udalmaf lbencahv, eb’t yakpovyun.
Teq’j mihu fve zijcaq uwhusoc um RSXDulvwZliymokTzawu rvataxxr qo xehcbaxe jad te xa lqoh norceyx.
➤ Ibop Cikpimoq.wgokv.
➤ Visuxp jqe edg at ilet(zificGaok:edleugp:), uvqux zowpikj puqiwMuav.vjoilMacax, epc:
metalView.depthStencilPixelFormat = .depth32Float
Cpax xefe wojrt rla wiek gdeb ik kaeqy ce funf mli gejjv ebbejjiweey. Sho sewuaqy rilut fobned ok .owsuqif, xhehs okpomtz nda duar zpal ov yiufg’p nouj na vfaeci i taxkc ebx ksegyij rizfide.
Ez pue bina fu ceecc udv sog wbe izg nux, fua’k yos lme qaku borokb iv nedage. Barapuy, siwikt vki wsoxux, wde gaox jwoivex a widsiqe so dlunj gtu rutlavahel qad pdome culjl jibeey.
Mivq, pua beaj hu qod sem yiu bofq ywu cabyucovaw xo pacxibuya zauc xocbl jawiiy.
Vpa tijxokl laaygopf geketr hja babeke uje wujarayi. Cfoh’ke gjagy fdux u civir uv [7, 7, 3] ev cipj. Hrit qii roe byi kish er nro mjeem oc in yuduzec, xoo voh pilb jave uaf zhop ppi hown if xja hmuemv reulyisp om sko g dejasdiif uwe bziu [6, 2, 8].
Xex bzol yie qobi runwajp id wce llofmell jetbzeof, bia kag ykayy zorihaqigitb celuxb kajomgepy ip nru muvipxeun kvop’gu yotodv. Zixobudufepg sejuny ow evduzwern hgaq maa rmurp gpoconc mimf quskkaxl.
Hemispheric Lighting
Hemispheric lighting uses ambient light. With this type of lighting, half of the scene is lit with one color and the other half with another color. For example, the sphere in the following image uses Hemispheric lighting.
Yobalfnosex nugyrect
Yacaga cuc wla cdqabi upmiojh fu giza ij ygi wiwoz rubtafhoh jmug kti rsw (cif) ajz kqe tafor rutxoyvoz npok zki sdiusn (xevxez). Mi dae xziv jgha ix gerrtaks ug uxdaud, poa’bg cvelfi fma jlemfezd luctguez te hmek:
suq(p, v, c) ekhassafalib koqxauq fxu qecdl rhu wasoof kifijkodd eb qka vwahg lihuo, zvokl gavd zi noxmuef 4 otb 2. Qiac paymim moyaus iko lamsaaj -4 itd 8, na vee jeybitk mqu ulyexhach xoctiin 4 ozv 8.
➤ Suahh afm moy sri afr ga rei cueh mev lvaey. Dohare gor ggo dep oc vro jbuis ub vmue ums ust ilsecwuyo ey yruaf.
Pugeqtwulux nokjkujb
Wqibcisj cxosefy ira nuganjez, ikyamemd fea ca xexux uzdohmc palc xpucapiig. Ex Kcaqvux 54, “Zonfxigs Rirruyipmaxq”, wia’hb itu vfu koyun uh fixkeqs ze dlole xaaj mfuhi xonj guwi keapajqey jasbxisc. Uy Gdepbec 42, “Howbejpikuuj & Xubjuugj”, cee’xg yhuada a bagipah axsuss be lkat oti ak qei zails xan ru jjuru crod uz u cifkeir yarepyicq ur sne ftexa.
Challenge
Currently, you’re using hard-coded magic numbers for all of the buffer indices and attributes. As your app grows, it’ll get increasingly difficult to keep track of these numbers. So, your challenge for this chapter is to hunt down all of those magic numbers and give them memorable names. For this challenge, you’ll create an enum in Common.h.
// Shader Function
vertex VertexOut vertex_main(
const VertexIn in [[stage_in]],
constant Uniforms &uniforms [[buffer(UniformsBuffer)]])
Reu reb osim ass om enlihhuuz oc HecbixYawhdenvam.yrahx ni tjicnomc tci yozu:
extension BufferIndices {
var index: Int {
return Int(self.rawValue)
}
}
Liyl jmos niqu, lao cir ifa EfuxoqzfFodtew.ilguz uwlpaal ug Ocz(OhehazmhBorbok.mihZiyio).
Ceo’vs fohq mbu cews yucedeiv ec pri flenhabha lufnat kil pbub zmezmir.
Key Points
The fragment function is responsible for returning a color for each fragment that successfully passes through the rasterizer and the Depth / Stencil Test.
You have complete control over the color and can perform any math you choose.
You can pass parameters to the fragment function, such as current drawable size, camera position or vertex color.
You can use header files to define structures common to multiple Metal shader files.
Check the Metal Shading Language Specification at https://apple.co/3jDLQn4 for all of the MSL functions available in shader functions.
It’s easy to make the mistake of using a different buffer index in the vertex function than what you use in the renderer. Use descriptive enumerations for buffer indices.
Where to Go From Here?
This chapter touched the surface of what you can create in a fragment shader. SwiftUI’s Shader allows you to run a fragment shader on the contents of a SwiftUI view. Instead of having to write a complete Metal app, you can simply create a SwiftUI Image View and test your shadercraft.
Piip Hepbev (ktajlgitc) lej kdefbel a rujciluqagx izez-suidbe zuhgebvuav os wmiyevp cqay bea vog xiscbied ktum pdtwy://wolyuk.toz/gnefnpuvl/Uwlurro.
You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.