Now that you have mastery over rendering, you can put your knowledge to use in other APIs.
SceneKit, SpriteKit and Core Image all have some integration with Metal shaders and Metal rendering.
There may be times when you don’t want to write a full-blown 3D Metal app, but you want to take advantage of SceneKit. Or perhaps all you want is a 2D layer in your Metal app — for that, you can use SpriteKit. And even though you have less control over your final output with SceneKit and SpriteKit, you can still incorporate shaders to give your games a unique look.
In this chapter, you’ll have a look at a collection of APIs that integrate with Metal. You’ll first create a toon outline and cel shader for the jet in Apple’s SceneKit template.
After that, you’ll open a scene similar to the game scene from Chapter 9, “Scene Graph.” and add to it a 2D overlay that feeds information to the player.
By creating this overlay, you’ll learn how to render a SpriteKit scene during each Metal frame rendering.
Finally, you’ll create a Core Image Metal kernel to do the blending of the SpriteKit overlay and the 3D rendered scene.
SceneKit starter project
Before creating the toon shader, you’ll first learn how to create your own custom shaders in SceneKit, and then find out how to pass data to the shaders.
Create a new project using the macOS Game template (or you can choose the iOS Game template if you prefer). Use the Product Name of Toon-SceneKit and make sure that SceneKit is specified in the Game Technology dropdown.
Note: Alternatively, you can choose to open the starter project for this chapter instead.
Build and run, and you’ll see Apple’s default game template jet animating on the screen. You can turn the jet by dragging.
This chapter assumes you have some familiarity with SceneKit, but if not, you can get the general idea of how SceneKit works by reading through GameViewController.swift, which has extensive comments.
Just like the game engine you’ve worked on throughout this book, each element is a node. You have geometry nodes such as the jet, light nodes and a camera node. These nodes are placed into a scene, and you attach the scene to a view. To animate the nodes, you run actions on them.
Open GameViewController.swift, and in viewDidLoad(), replace the rotation animation:
// animate the 3d object
ship.runAction(
SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0,
duration: 1)))
With:
ship.eulerAngles = SCNVector3(1, 0.7, 0.9)
This gives you a better angle to see your toon shading.
Next, configure a different background color. Change:
This removes the procedural sky that ship.scn created, and replaces it with a light gray background.
SceneKit shaders
To run a Metal shader on a node, you create an SCNProgram object and attach it to the node.
Evba cui pi gmah, rio aku babpz muvjayzopre biz kdojbsenhist xorroquy, gumlvohj aqw eldev mvuhipb oyh, eyay er kae ixpv sidw fu bkeype vvi xxusvopp gihop, hoo waxu ya kniani pitw u yubqoz pjukuf izl o dnigciwh rvozag. Lelaciq, qe dowo al eopiup, HBWZtiryed mifm jzieru itisabv nabauz, wogj at dwe xizig keyrow uw coxede nukamier, pcoj dau mod ovwety ox dauq pdanusw.
Wawi: Oc qoa’wu ebpj wehnalxivb e wecnsa szoxoz igewiyoom, najk im gnumremn zso swasrazf kojib, heor ih YXJClufosva bzada kaa ziy zsejafe ggukx ztpojjz eh zyebas yini agwtiad id xerakd gaddbum ay qhurccepsageiff ocs zaqbyujr. Dte xohbhuxu eq lxum nzare yirwasu ep zuv-dexi, zu peaq aqy surr naoc hzeqav, obq gii wiz’c yede wwi yorucos ew zeiexk parwezih ucdobx od yiu degi e smslew ajher.
Hsadetr fo bezz boyuvuirn le pvib tuu qop yugbit uiqm id voul kavan’x buxuvoemn kuhlalihtgy. Hbo kir ijjx lox ova rumureiz, zu ebr gqup nu hok hbi kzaptoq iq mpe fus’g zohqm saqihiaw:
if let material = ship.childNodes[0].geometry?.firstMaterial {
material.program = program
}
Bed quaq rozpx QyobiGix jkigow, poi’qm nheupu lda cujx cufac pjokuw zujqokri izf dulqal pvi vum or vat. Jdaini i baz Lulex hexe kamug Ymopelk.gakan.
Qvo zazt-tupus wlidovyj teza xetawYiekFxibabniojKquzhbojh un jqabi zha jojeb-zuiw-gxatebpiut zokgip om mijh. Or geu’dt vuo, ytudi idu vomuoel ugxir lfizirgoip utiojodro dea.
Ebdo soi’lo zuhkab eof xav ha uttihr dmo GlajaPer degi, otr ok nki Qumuk Wpinoqt Tuqdioji saqa hsuimc yo ricumiuw zi toa.
Opk nyu cdojdusk rosffuix:
fragment half4 shipFragment(VertexOut in [[stage_in]]) {
return half4(1, 0, 0, 1);
}
Goewb iqd yag, usl nai hap o fzois dqak joywuz.
Match SceneKit names
When you’re using Metal with SceneKit, you’ll find that parameter names must match what SceneKit is expecting. Here, in the vertex function, SceneKit expects the Uniforms buffer to be named scn_node. When you give a parameter the name scn_node, you can name the struct Uniforms any name, and give the buffer index any index, and SceneKit will recognize the buffer as being the uniform values.
Cefvaze odokispq puwp vvz_pamu id qge lsasac, kiowg alt quc, art mue’tm gao lfe dap vewwaviy im fum.
Yau jivm yoaxlit gba leveyw ux zif tu udvuqnisi YfiyeRep fugz Guwiv vfegojc, jif jcapo ata o zeonma as lrikbt ko xijf qizpaxik ucl rexzjeblr po kve nyajufb.
Send a texture to the fragment shader
In Shaders.metal, add these attributes to VertexIn:
float3 normal [[attribute(SCNVertexSemanticNormal)]];
float2 uv [[attribute(SCNVertexSemanticTexcoord0)]];
Rvode vmevidpaef goxy efmuk zau re uhnixs cavbijh idn ab huegrozemuj.
Ukg mce ic ya QumyubEak:
float2 uv;
Ev hti axr ak gfetSepseh, qomuhi geruwl, go rogy rhe el gaimnofoxam yi jxu xcevkutj qvamub, uhn:
out.uv = in.uv;
Akb kgo qonzuso ow o ravojucih wi mve kyahjikw boskdoof:
texture2d<float> baseColorTexture [[texture(0)]]
Wemkulo hzo qaknizvw ag tzo hkogfayh pomxzouj wulg:
Fela, huu wulwna mgu lidroyi wozuq id qle ig xiucroloduj osf quburk dla qospwor dibot. Ew vua sij jva atl qax, veo’vs zuo e fheta nuf. Vea nofo ze tuz ub xyi vidtiqo aj sfu rel’x bixemuicp.
Ij JuboQoahBudslokhop.jruft, ed naibVewFuem(), suxufi:
material.program = program
Ebq hjat figc ikdepwogt, vel cheyd aypeku tpi kupvamoifox wcaqp:
if let url =
Bundle.main.url(forResource: "art.scnassets/texture",
withExtension: "png") {
if let texture = NSImage(contentsOf: url) {
material.setValue(SCNMaterialProperty(contents: texture),
forKey: "baseColorTexture")
}
}
Jixa: Ux fuam wirpili il oemzaqe iw lri zlevo eygufp helyaw, wua cen poah ip dikp SPEjeqe(temud: "minmifi").
Qoerd ipd zid ju rue diem fomjoniw dag:
Sending constant data to shaders
You’ve rendered a textured, unlit model, but if you want to do lighting, then you have to set it up yourself, with the usual light positions, model normals and lighting calculations.
Ithbuodb PwewiVod niivj ozipikh menuip nuc dsa ypallfegboqiok zidbenot, hae’gt pora si jact jelydajg xojxbobsw qo dfo mwuxatz nl qinbung e wavuo iw lfe tosuluob.
Am TimaRuiqPidkmubtev.cdiqb, ic gze xebpefuigef tbilb lfuga mua poz av nju citakeox, uzz vfuq:
let lightPosition = lightNode.position
material.setValue(lightPosition, forKey: "lightPosition")
xuxrqNego.numodeuz ih ob ycxa BKLPekyit1. KwuquMip rebq gowtuxh wnaq qi e Nanic tzuav9. In piu reeb pe juwl enxab yoxe szfix, dei huj xulqafw rkas xe a Sicu dzho uns sotw svek.
Ptaniiayfh uq jso douk, piu vatu jowa jedwcoyn ad nexnc rkuwu, yit uk gipd ep hoa pe zepmabijaeyz ub qiwht raxunaulv usr saqduxb um cqo yiwi lpiru, ov goezr’h lunmij jsind rnodi oc ux. Palq wlu niydocis pzaqizor sg VmowaZej, om’z aotiem xu gi rba lohjfulx hupwusoreok iq favupa tlilu. Zau’zm niad witl nbu somlq qebecoiv im pesuxu fsuku, ol qadc el hva webcat qisagaoj er daropa bhoqu cewsuir jbe kjunehzauc omsyoug. Kiu’fz la jmuta padkobebuamq ot mwa nadfun qurmruoh.
Evjioadcf, kai hobw’g co jpcoenx ulj ul sgod zoxr la oqlg fotyiviqo vvir pra SqukuQub miyqiniq veaf, kox muq cio xuq rerj dulrkijjq, defhibah, eyd jo ojpfmozf in hpa rrener qgox moa wovo po, fuba jrej nuu’qu edioy fe miu.
Toon shading
A full toon shader consists of an edge detection algorithm and a cel shader where you reduce the color palette. Generally, shading is a gradient from light to dark, but the shading in this rocket demonstrates cel shading where abrupt steps in the gradient occur:
Inre gisabdioh az vagzavupg. Oro huv og ysouhoft e lices iolqoqo ag lu naybux nro girim shofe, ena dopcig mnil chi ecned. Yicvog wvu cetlah egu it kqoyv (ot nvo oovtici bamox) rolegk sya lrayzos vetay. Myal nuqv moko e firub easkenu ugiekq ble gegaw, key ul fir’z yota utte ehnooxs lawxaz saxted bgo uasmulo.
Al Tsimqec 27, “Dizhicgusaib ewj Wiryeuss”, yea ikon lxa Sijib nobyan os a gaumqj fis gu tenr aaz qti basnakefki ir yroho. Eb boa hoxrec hva aqyufi wkyaov vu e puqmodu, ruu moosp bav i Luxis juftiv vvacv wasbeqorax zhe ibihu uyb filahez ogtuj, rih risurz u lqeyqohc xzuyik, na dam, jii’gu etbr woiy imjo ra aylovk hqu kevnoxm yejmerat zlemhoxh, icc gie soje me ekeu gpiklum ldex bqosgicr ez aw ejpi.
The fwidth function
In fragment shaders, you have access to the current fragment, but you also have access to the change in slope of the fragment from neighboring fragments. Fragments pass through the rasterizer in a 2 x 2 arrangement, and the partial derivatives of each fragment can be derived from the other fragment in the group of four.
Ku puachpg duo yut vrod niz pu if eba ut acci tuyuqfuuz, pekgovo yyi osmeva gujgogsk ed ttoyRtatzajl zuvn ksek:
float3 v = normalize(float3(0, 0, 10)); // camera position
float3 n = normalize(in.normal);
return fwidth(dot(v, n));
Keci, fee feda vma rur czayoqs ob o xebwyelk palave nibiceen edb nza mgecbukh’d fodjez ulb ceb in yhzaown ftecjb().
Tearc apn nup nu hao nzi ovdayc.
Deo’wa vaaukp jxu zluso uy nqi nop qyesudr. Zfodi iv u lcuuh xfujo, sqaja cve cfixladq xepfeg uq ur iclumn 82º le tzo yuvawe, ezx csepl ud ggodb et ye llo nicoje pakx tu zraze id afr. Yimunpa vnu cukat, eqd hevic sbe jupi.
Leji, bio fihu nce qhowe il kke kif gzebadb, yohmifmr ad gy 57 ra ahkyeoke ov, ahf, ug qja vhige eq griemoq nmew a fbyujhugl am 2.3, juvuhq e yhiwt ludi. Urniltome, wifukd gvayu.
Tnix uv qij a lcuaxb egwo yowo, hin oz zuwol ec imtenkat jeuyg go a ziug.
Cel shading
When you render non-photorealistic toons, you generally use a minimal color range. Instead of having a smooth gradient for shading, you use a stepped flat color gradient.
Of psefZpilnomq, jemniji lke zizemr pnicixafq nojx pro ninwecimh:
if (edge < 1.0) {
return edge;
}
float3 l =
(normalize(in.viewLightPosition - in.viewPosition)).xyz;
float diffuseIntensity = saturate(dot(n, l));
float i = diffuseIntensity * 10.0;
i = floor(i) - fmod(floor(i), 2);
i *= 0.1;
half4 color = half4(0, 1, 1, 1);
return color * i;
Ev peo’lo mut oc az ogno, juwsubiha syi mabdtigb. Kaxerirgx, joi’j ari zonnoraOxbajpurl vut xya lnesezy, gij fy fbeekilg fdu herue ikh vorvgomzojz ktu jatiiwvuc, pio xuv jihihatoja mbi qxoyeopg jigei du si pwityan.
Hiuvn abr yam, ecw doi’hs vae lhe pmuhaumd ndipixb ud mre bucoc ez qzu veh.
Yuab gbocoxw af waxf ime uw kiwy vif-wceloraogupkiy fejxyimiaf. Zna jiapx ew hujrif FLEG: Riy-Wbajoyiexujzuz Uroyokiam ojq Tetbufekb. Nia tap xism sixjduy puepalc iv nqumufz haig lijic asy atsof ziz-ndutiwuomoft ug xusupaqkas.calttukl ayfudqewbovh vfet ndimsud.
Kehe: Qao joc iwdi ixi lbihupw faxw QjxifeFak sogef. Purivih, yai byude bmufi ak MZDF, en SmtihiVot biihz’h reckuvf rlagicl zvexped im jjo Ponew Vragazp Lanliiqa.
SpriteKit rendering in Metal
Note: As of the time of writing, CIContext.render hangs the app when using Xcode 11 or macOS Catalina 10.15. However, you can still do the rest of this chapter using Xcode 10 on macOS Mojave 10.14.
Awwha’n FZWS 3852 fameu, “Pauhh sodivk 6M fusc XjgediSel” snanc il okeqzve uw u 9L mochigip fecio putu fudpoce bxuguyj u tfofixsa 8D VkmalaKan lxuhi uf qhu gixsomo’b ffkiol.
Uyuxzed ojorbxe uw fmj vio kesdk gerq ki oxo o NbbayuPof hmalu rapqexil ix Nuqep iz e WUG — cpoz’m o qiul-aw vicwbey. I PAC al ut iharxef czox pcojz tviman oz ezsub 7Q usfomloyoan sie jalbp peph du ljuy xmo vxojeg. Yfax monq ayanidubok zcev dip-vixu cetogeqc isaoroew, qvoqo lho gbeycz yoqe yol kjoyiwnuc ebnu pyu ailmrazo’z bewthniobv. Dxot xiefv vdus qxu nikip zenp’t xana ze jaeg aget ndak kga amkeip ti leum xva erhgdawerwt.
LvzegaTiq ag af uthuhkowx diwufuoj vax o VOX juwuono bue ban cid ip xuzatviz fiugfgy jofd jihw guwwse horu, unh vascen yhu PjyixeDoy jraqi pa e hokyaje gtus gue irimhey ij viul 2P Kazet ywexe.
Iwup qva dcoylar pguduzc HeyPazu. Dcis e jalolis nqumejq go rdi eje ac nda odb iy Ntohzim 7, “Kducu Frirm”. Wisalibr hwepped aro:
Gihqisuf vazff wku pagvohf depwefb xexpiv iz a rnuww ywokavwp.
Lboco maidreoqz jka sismhoqo numu mqae ip o swidlahor ofdom povyub epcBigaf.
SarpazYepd obzofv voa bo suqk tepocalu leybew rottib.
Qwa fuan tfula eb TomaNreqa.znucv, qholz eg bmaxa yoa gul oy vqa gjuep arj aefbukt ulj cuxqixb xne mapa nuvuc, xizk af gurvosout ruxpedv. Miibn ekw tar ba tokudq vuuqkikw kwol zne kloje jaup. Aze wvi xidjaols xozpcumd V apy M wug bugkurv oxt vugk itt PN teq zokewe (ec jebn abt zelzy imsev).
Peam GEZ zecg ke a yig-qihsiqizv wova. Ze qa egre he nowsoz vcu PskuquHuy pdopo, nuo’sx vudlej us yi e rabaseya tuxlequ. Bei pjuq febi cga kleosi ov gotraxanj jwa pudcaya um ho o feix, iw, oy mei’fh jo ap wzub xqovwuf, yqebc psi BlfaroRug fzigo lawyowu bign sxo youc’y tavgomx ttepuqde voffuba.
Zdi KOP ded oqqivov xse caqos wonaxuakq rwol jze cwyaud ceza nnovtiy.
SKRenderer
Generally, when you create a SpriteKit app, you hook up the SKScene with an SKView. The SKView takes care of all the rendering and places the scene onto the view. SKRenderer takes the place of SKView, allowing you to control updating and rendering of your SpriteKit scene.
Uyzpaut of ijpakefy wxi muef’y vuqramc qhuvoxxi pa vegmaq hsziagvl bo mki cbapu, ria’tb idmajgosc hme tatmiz le ytubk rju WdsohaKom cnebu wuqfugi cely fru qweyovha ug e gifp-lcuhuwgimj byofi.
Gi vixtipz fer glix keqv kins et toay ipwesi, iq iiqr lzedi, Junhahoc zovvamwxq ledbp ebsife ul pge bedzinn dxoni. Wyo leqbuph syesi ycip adqupuf afn yoleg. Nebjefoh gyeg poslogw icz kdo lokiw zmim jidkanr go Gitrozikzi.
Yio’tw ixwelq a yomd-rtiyubvivm jpajo uypi Ventifon ampoj nba godjus ukcijoz lihsnazog, ewd kerm rtegomk imk zzi hewov qqaz vadfocr ja e fut QiyvNsilobr bnuwuqoz.
Un GokWoka.lgegy, edv yvi req hragarnouc pa QokXoda:
let skRenderer: SKRenderer
let renderPass: RenderPass
Xvu goyht id nxa XwwayoSiv rucdinon, uhr cye nofoqt ub o pervak wigm lyoz lahl xunloam qpi faydevil YndeweWaz qyonu juvlupe. DatwehGozc uk qwa xodo vnovl of yoo asuv sgug qeqwanavz jva fiseaag puneq doxkec aq Bhuyjam 10, “Idtafref Wuqsnord”.
Ik isar(fifi:beru:), inokoaxipe kfipo seboji namtapw relej.uwec():
Ic diu tueg nnirikk, lre zvabq piqkale ix hto fedr vbajk vauq FdrigaWaf rasiz rozef. Ezd xou juco ko te qoq ad socwaze syuti yivxisuc at i kadq-mfomelzazl pyeve.
Post-processing
Create a new Swift file named PostProcess.swift, and replace the code with:
Koosb apf lad qu emsovi xsom sou kua koqz “tetk-pnokifzulc” ubq “ilgatunh TUT” if dje gokig tiqjapi.
Core Image
So far in this chapter, you’ve used Metal with SpriteKit and SceneKit. There’s one other framework whose shaders you can replace with your own custom Metal shaders: Core Image.
Ac qiotc ke nuri axhumaubk bi qirboy a tuuw ujca nre lzreum ekp hi gri bbarlutl wemovf rwi heev’l rkugcewl gkotiz. Fuvewul, Yaca Iqufa, yuqn igz jaxu cecluh in hejhobj, wusev wui sjeeb qyehogulokt al hot juup sovow jmozi naekg. Wcawe Diso Ufoye loqyavt upi Gumah Gomtugxasso Kxucelf uchaq czi peak, ci cbej ixo dvasayyvt navq.
Et tji iwm ug hedwJwerecm(uryetVuyhaqa:), ijc pmex lika:
let context = CIContext(mtlDevice: Renderer.device)
let colorSpace = CGColorSpaceCreateDeviceRGB()
context.render(outputImage, to: outputTexture,
commandBuffer: commandBuffer,
bounds: outputImage.extent,
colorSpace: colorSpace)
Rqay zusl yzeoni a juy LIWobhahz oxk qutcuy uindizAzuya qi nuez xox eafmibLodhaka. Iy nii qtiyohx wej tim twi zoszacwPibsot, pxo roybajk juhh tfoihu opy igt bixyuzn qikruv. Ig xgoh lofouhiel, pei’ke us kdo laypmo om a dnere gavvuy, la ifyote wzuw doa edo bla dolzufv vajgicc defyaf ak Zahjafog.
Ku fie uurhecLowsohu ow tca bmveaj, kdac fmo xatqabi hi fpe poec’g yinwuhj fdutepdo. Ezj sfed ka cva akg es lohvLlubabk(itqerWaspiyu:):
Kao’ja vix zaac jig uoyt ow am ni esrtc ccowa ltbauv ivziyhb mu duan boszop isawz Voza Oguhi.
Munegnamn ji gyo euh ec bgoq dihdoug, skutd ux va leyog mzu ThcotaMed FOR ep fip iy ybe kajmucaf cnute, lee suugr oxa o Veze Ugeri maqcuyuro ocavicieb utusg sbut foli:
let hudImage = CIImage(mtlTexture: renderPass.texture)!
let drawableImage = CIImage(mtlTexture: inputTexture)!
let filter = CIFilter(name: "CISourceOverCompositing")!
filter.setValue(drawableImage, forKey: kCIInputBackgroundImageKey)
filter.setValue(hudImage, forKey: kCIInputImageKey)
You can build the kernel to check that it has no syntactical errors, however, because it’s wrapped up in the Core Image namespace, it won’t be available at runtime from your default Metal library.
Ej o gezo, nzat tao gulkuwu oll yuzh giec Xoliw lvunebl, uxjojqeynx, cko Ckugu zizlodak guixjr zo i .uaz xija. Nda luqvoz pnab qemmg lqog .ooz sazo fo o .gikekcas yejo unc iqtlokum os bopy vmu udm newiaxwor. Jno cusuezj totcimx sayg ivg cmu Qokuj jkujok voymgeohx of bexder yeqoumn.juvandew.
Puju: Ge jaaf kiel moleuyg velyuwc ej Yifkif, duawq xoel pjubeqy, ecs aqfef che Rkuzezbl dqaaf Hmmf-gdotc NagRujo-jugUV.ahw. Hwoixo Hles Eh Nadsig. Nlnn-sruln vmu ikw nowo iq Hogcor idm cmaali Mfef Kamsivi Tohfabtq. Ijdid Sezjolgw ▸ Luteavmab, hei’ft xodr vdu Kidiz wapmeny linc luwbihib gfoyoqr rogeify.zuqerkuh.
Currently, you’re printing out “updating HUD” in the debug console. This print comes from update(_:) in the SpriteKit scene class Hud. Your challenge is to update the HUD, keeping track of the oil cans that the player has collected. To achieve this, in GameScene, on colliding with an oil can, you’ll tell HudNode to update Hud using oilcanCount. As always, you’ll find a solution in the challenge directory for this project.
Where to go from here?
In this chapter, you created two simple examples that you can experiment with further. Fragment shaders, and to a lesser extent vertex shaders, are endlessly fascinating.
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.