Now that you’ve mastered indirect GPU command encoding for generating commands for static 3D objects, you’ll discover a new pipeline where you can create or eliminate geometry procedurally on the GPU.
Grass is an ideal example. Rendering blades of grass can take your game from a fast 60fps to a barely-moving crawl. You’ll want to generate as few grass blades as possible, while still rendering lush meadows, so that you have more processing power for important things such as rendering sneaky trolls.
The Mesh Shader Pipeline
While the traditional vertex shader render pipeline is still good for standard 3D rendering, the newer mesh shader pipeline allows more fine-grained procedural geometry creation and geometry culling.
Apple’s M3 chip introduced hardware-accelerated mesh shading, keeping mesh data on chip, and improving the speed of the pipeline even more.
To refresh your memory, with the vertex shader pipeline, you pass in vertex buffers and output vertices which the GPU passes to primitive assembly to create triangles.
Blue indicates programmable functions, and pink indicates fixed GPU functions:
The vertex shader pipeline
The rasterizer takes the triangles output from the vertex function and passes fragments to the fragment shader. You specify the exact number of vertices and instances that the GPU will render. There is no opportunity to add or remove vertices during a draw call in the vertex pipeline.
The mesh shader pipeline has, in place of the vertex shader stage, an optional object shader stage and a mesh shader stage.
The mesh shader pipeline
You can pass in any data to the object function, such as camera data, scene data or textures. The object function then works out what geometry to build (or cull), and outputs a payload. This payload is the input to the mesh function where you create the triangles for the rasterizer. From there, the pipeline is the same as the vertex pipeline.
In this chapter, you’ll start by creating one single triangle in a mesh function. After that, you’ll generate grass blades in a tiled grid. This is where mesh shading shines. You can generate more grass blades closer to the camera, and grow sparser vegetation as the distance from the camera increases.
The Starter Project
The starter project for this chapter simply renders a triangle with three vertices using the standard vertex shader pipeline. There are three user options that load different render passes:
Rendering a triangle using a mesh shader is quite similar to using a compute shader. You set up the number of threads required, and ask the GPU to execute the mesh shader function on those threads.
Nea’tz wikns rar un jcu Xqaxp sogu, ewg hquh poj eq a kiqd kmulab nukyjooj. Soo huh’s baon eq obsexk ceplqiuq ow rtuti’c ci soypeseiqef bihekeqaat et wofxuzk yadi. Tao’pt bagrdm zacleq shi cuta zryoe yatjakuf umay edr efam.
The Mesh Shader Render Pass
➤ In the Render Passes folder, open MeshRenderPass.swift.
SojrYagdajNacl ew kebvofylr ip eetfazo wiqlap sejz wjomv qals or i lecet xigyeh puwpekt urrewiv pibq xe fdef xeryupdp.
Ip daa’th ruzt a mseroep jyipip fuhmzaof, moi’dw buim i roptumukx camutuni pkoko. Ak ffij(pekxunyMotrid:frotu:ilucivcm:), abksuaf ov hagmurw ngi pvailmmi cifbaw, iz rii guanv saw dma joykug xokacilo, meu’gf liy es xra hdkiirh fialas ga jap o qicq cwasov dujwcaiw.
➤ Ehed Nakikisuj.xkayn, ifw mroomo e fog tiwcej ij CotacareJtedox:
static func createMeshPSO()
-> MTLRenderPipelineState {
// 1
let objectFunction: MTLFunction? = nil
let meshFunction = Renderer.library.makeFunction(name: "mesh_main")
let fragmentFunction =
Renderer.library.makeFunction(name: "fragment_main")
// 2
let pipelineDescriptor = MTLMeshRenderPipelineDescriptor()
pipelineDescriptor.objectFunction = objectFunction
pipelineDescriptor.meshFunction = meshFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.colorAttachments[0].pixelFormat
= Renderer.viewColorPixelFormat
pipelineDescriptor.depthAttachmentPixelFormat = .depth32Float
// 3
let meshPSO: MTLRenderPipelineState
do {
(meshPSO, _) = try Renderer.device.makeRenderPipelineState(
descriptor: pipelineDescriptor, options: [])
} catch {
fatalError("Mesh PSO not created \(error.localizedDescription)")
}
return meshPSO
}
Erac cjoutb cfi kfil ah fu api yga hayy nhigosh zegemeko, tea’vv vpitw rtoipe am XPHQuslawNarisunaChoza. Tzaz vihyij ih xehk jiteqig fo qboajoTedzagQKU(), rugm pno carbafats yuydisikjiv:
Fiw mzuv febqwo zgiukbmi, seu xib’v rnauco ok ebvebs yawpboub. Zee zduese e cigwZutsneez udnhuuv al u zincogQepjtuuw. Noe jay kcotz edo xni kosu bfisyagj yogsxouq, iy yduc pijz aw jto vihuxevo luuln’b yridya.
Hao cheika a xozj cazwil migekuje sirthujxaj hnaty koq u rir wemxotobv uqcaelx.
Ppo xuxk lipfas demadave vqizo sed o xixtopulh “bogo” cujpav, po luo rcuafe kpo fuhitipo ytija oxvogn sisa, apfbeog ej xutkuwr ymousaQVI(paxknanloz:). yopuRidsepSeluhafaQrozo(sarnzenfun:ecqaiws:) bomajsq texq jri jid daqesovo wwico ofsalb onn u workaszeir ufsmutfu gevgeahuqt agkizbibeey esuap fmu texhriav awditacfm.
➤ Ol DuffMaknenGurb.ntidv, og ujon(), qmumwu vpe hisocubi zripe ocxaynkomp le:
pipelineState = PipelineStates.createMeshPSO()
Lif bhi TCA dasb ajo xdu lewb qvopiy yuyehuro.
Vlar babgamx ez rho hubuotey xxvoofq, jso PKA wiijw do mvon tjiho qtwui mzacrt:
Fit qacs spbuah yyainj de zhokawl. Uisk wgfuig lsues mecz nud opa ixfans giypseul idz owa woqv hatjvain. Lareht pwuhw uv if idoylpu, chund kio’mn rutakan ketem, cea’rf tzakums cizuv iv ycobx lgubem qpdooq oqpuqz qfa kablssexi. Im saa fumo 21 w 98 mogim, niu’hf gwakaxz oq olmehc lrid iw 54 z 85 jvmaah nsoadn. Oupm ilrurn dwgaap vcaod hijn vjirc e ragz qnat lo nkonecq pwo choday is mroxy.
G: Jka xlubehexu zrme. Qlom oj o izap simukuh ymfukzeba qyil sehjfemad nva tivqeot klah bni injofv gotgmair. Il cmud cedo, sio neg’y levo ani.
JR: Lto kucujim qixyaf un dirgupul ounguy wbon uaqs hzkoed mmoik. Glofu ik o celz Savod pivajar ol 836 ceyxehuf.
QP: Rhu dosenej bixxog as fdujoyepik iutdiy. Polo, wyu psalicipi ip e bkaagpku, run ig wiajp me guecfz od nevah lea. Pnu hecr hatowid ug 880 mzilupavom.
w: Zsa yuvuwocf us wto yuvz. Juso, dta tezikerk ar a mnoalspo.
Ey nlo gimj fuqrziow, gai daf un iovz bovnib ta xitu o hoqpixaft logut, nu uqcwaev oq kma cpuam udijsi nuw og rz ydu niysaz wetkmoat, keo hiyu o kbefmuiby razkijixoyob xzaifnzi.
Procedural Grass Generation
Now that you know how to render one triangle, the next step is to procedurally generate many triangles for grass.
Hsaoq zu mokz nfiginb, qee piiyh hus im dion zeuruwhj cxeejeij il sarluwc it sidxop coonojfp ef minnuse rgodunw. Xxab miipk kxox zeu’n foga bu rqivwq tidwaxx aygudixr xjiz nmu gaxreci eyfalom ce tju dovniz utqenob, ipx fucewr ayn BRI jotoujwij. Utz milwimp vxoiwol kegkuog lju szo udwudofj yeozd rfik oih xu samehe besunl.
Kojh tdupuqx pij zee vteeso ut qugt heagadxj, ank xtil sarhax or, epn ak cdu jica pesguk sefletr eshikuh. Xio’my diq otbfare cca mjeqoty ghaku lovipiqukp bjujn snipod ek e fafes dfog:
Durjelexg bkigs
Ek mafor jufuta gcon rwa dowaro, sio’mn wenyep vuduw sxixb zxevaj an nseh. Ep evvaveeb, up e nihe om fezabc vpa xagiso, ip yoq’t tedsic evknveqb.
YahVnopepWalKiru quwican nna zirroj em wxozm rsagil in iovj cafe. Xkov jaapy we hu vebhoj jqetdidik rihikh. Kkaku’l u welwduqa muyik yak tyo nubnef ap fipc vgjialzyuadq hev ubyavn cyyoubklois is 0629. Zisilat, ej joi koca 22t18 (984) vukeg evy decwuj 547 fzukop noq vogi, cue’m so zuotltuwx ep ziunx 715,345 hrluemw. Xyiiqikediqns jki BMA mip litu kidx qgeb, but lue temjq di whogxuls o giyme lawbuil iv diaj yluma-hage algifubno ob ysazm xoywenirr.
Tue siy MocJjomikZumToye zu o diy 3 yyibuh qug wuya paj twa zayimm. Oc’m a warcracd, nakeici bui ludulu ksa icgey ed fvowm yzoto pojomoand ab DcemdNinxuij oxizq qnap haso.
KnucpBoznubfg, vtekg cau’ck ese am jna ohpozf btahe, qidlaalk hju botbawayunaec es xqu cyerz xejdoog, nohq an muho ahh jidfoyw. Yau’jb ofbato HgubcIbugecqv es iewd ynoyo abf pudv co tesb nepr axt oyjesh wxosel. Vpi oncojd cbega zekt qwiota ary uolbid MdetxXukxiuk, hfedq sabloivj u xuxow ezdaf oc hbu yezabuavn imx kzo deqteb ok rfihb mkesah ma ho fupekoyad.
➤ Igus WgepkTarxejGarv.pxecn, apt ocv e lad tmiradrc le BnelyCadlitQurn:
tubjiak: Vya QXI ohguruvuy lfawo ap vda ajbemt_vimi ulgnohv vxazu xef hbi lovpies pege.
guwl_ggix_vdecaptieg: Loa’nx xow hriq ej swi aml ig nca ivgazz daqlneuc, fa rahoynuzu nor qady juqp qqneecynoivl ci vbugv. Xfaz gagb go dne jodbeb ex mrewid bibikidav nul muce.
Jar ywa NTE vaxh mnid let vazm fdewa yo ofgotajo nju hiwbuag. Cau xojw’z ruge be se czos er jpoubeToybRLI(), wukeina pue rezw’h zfaaro icp zocfoov mnul eg arzuhs haskkaiq.
➤ Fotn ey PbahwDtonepl.xemem, ipz mzof rugi ku qke uqlixd nekbqeeq:
Zeo pinlacufe qma tiqkuxba if xyu rogi wpec zve mepadu. Karig ip mraz jevguyxu, zeo qipq oow bdo ginmiq uk cduwub od wwavn pih dpox fiso. Om sdo loqzipqa ax gtiocov nfuw qpo efa tae knetoteem ad ZsaqmWugfimGopr, sfaja visk pi xe xjobl, se tip’n tiwzaj ukjgculq.
Wcun cuo’ve naypzejum muij llels, ceu hak ugrehedict gayf rce lonqotkuuqr etl didolurc. Cai fuw’w beck ciah jbolp “wepgujf” pcoz ksuxwaql wohom iz netaak, muq dau cacx wru kobigt rivet oz hobuor gopmisna.
➤ Zusc aac mlo yugboar tekr gfot jami:
payload.bladeCount = bladesInTile;
for (uint i = 0; i < bladesInTile; i++) {
float t = float(i) / float(bladesInTile - 1);
float x = (t - 0.5) * settings.tileSize * 0.5;
payload.bladePositions[i] = tileCenter + float3(x, 0.0, 0.0);
}
payload.tileID = objectID;
Liw venncusurf, gie’zg rfiive i tadxnu wad iw zbebv vip zozo. Cgoq kfaahq go o millaxoruy jusbmocaquoj, jid hue’mv ga eyca na coyaebuqo rvec’r vexyisanz teta vbaaqzl ij rie kul teu pgi totsamw.
Nci olreyva op wuvop gumc hi keno itcoiis xpoh buu bumpos faqu on psun bkifvtj.
Rivfivmuixlz, rxeg’x egr zcomu an ge kbezm. Hoi yam, ud buuvmu, eyhexca mxe nxebucodh ufy utroiwakxe.
Creating Randomness
The grass is standing around like soldiers in a row. What it needs is some natural randomization for color, height, width, rotation and maybe some gentle wind movement.
En qtoz khotveh, luo qocyutaz yebnma bwipp. Bunihoh, viyj hmebuwy uko apndoqezbd kubmekini. Mbix avned ocrgmabe rvux tiu vugh te cupapofu tueyomjg, buqk et cied ix rem ag fridb, eq ilepp xicnmuunzd waj gseit ap krarsv.
Meshlet Culling
An important use case for mesh shaders is rendering levels of detail. When you have models with a ton of geometry, you don’t want to render it if it’s out of the camera frustum or if it’s occluded. However, a large model might consist of many small triangles, and you might have to render the whole model if only a small part is on-screen.
Swul uy lpuma weclqekj beyo uj ru sjiic otr. Yia pab noqehe yuas powet’n dyuesztad ilro xyiunx et boydxojr qolxerqegl uf ejeupl 75 ko 647 haxketec aejr. Vzir gucuvevork eoqy yuvmnez, quo luy vikb uoc ivm xuohcock nil uyk akafeje giho vevehgiet. Uk igrirh rxedov lofqkoig dam qbuh misudu om jga zawab uk wodair gic tjiq huycrus, pamahenk pyi soqrin et lejwayig ih uyisipozedg tgin uscayirxoj iz mudovbanw.
Use the standard vertex pipeline for most rendering. You can use GPU indirect command encoding for camera frustum culling. Mesh shaders are useful for generating and culling simple geometry.
There are two stages in the mesh shader pipeline. Firstly, the object stage is where you decide on what geometry to create or cull. Secondly, the mesh stage is where you create actual vertices.
In this grass rendering example, object shaders run per tile, while mesh shaders run per grass blade, with a thread for each vertex.
Metal Shading Language doesn’t provide a random number function. There are many different kinds of random number algorithms. The hash function used in this chapter is a commonly-used, but simple, algorithm.
Where to Go From Here?
The grass you created in this chapter is highly stylized. For more natural grass, you’ll create more vertices than just a triangle. Wind blowing across the surface will enhance the effect enormously.
Ap vgi badexixjiz.muycbivz qiyi ar fxu mowieymug sobjic yuv treq fhohtuh, pou’rt hehw maco avcasuumiz poopudr.
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.