Velkommen tilbake til denne tredelte serien om å skape stilisert tonevann i PlayCanvas med vertex shaders. I del 2 dekket vi oppdrift og skumlinjer. I denne siste delen skal vi bruke undervannsforvrengningen som en post-prosess effekt.
Målet vårt er å visuelt kommunisere brytningen av lys gjennom vann. Vi har allerede dekket hvordan du kan lage denne typen forvrengning i et fragmentskader i en tidligere opplæring for en 2D-scene. Den eneste forskjellen her er at vi må finne ut hvilket område av skjermen er under vann og bare bruke forvrengningen der.
Generelt er en etterprosess-effekt alt brukt på hele scenen etter at den er gjengitt, for eksempel en farget fargetone eller en gammel CRT-skjermeffekt. I stedet for å gjengi scenen din direkte til skjermen, gjør du først den til en buffer eller tekstur, og deretter gjengis det til skjermen, passerer gjennom en tilpasset skygger.
I PlayCanvas kan du sette opp en etterprosess-effekt ved å opprette et nytt skript. Kall det Refraction.js, og kopier denne malen til å begynne med:
// --------------- POST EFFECT DEFINITION ------------------------ // pc.extend ( pc, funksjon () // Constructor - Oppretter en forekomst av vår posteffekt var RefractionPostEffect = funksjon (graphicsDevice, vs, fs, buffer) var fragmentShader = "precision" + graphicsDevice.precision + "float; \ n"; fragmentShader = fragmentShader + fs; // dette er skygge definisjonen for vår effekt this.shader = ny pc.Shader (graphicsDevice, attributes: aPosition: pc.SEMANTIC_POSITION, vshader: vs, fshader: fs); this.buffer = buffer;; // Vår effekt må stamme fra pc.PostEffect RefractionPostEffect = pc.inherits (RefractionPostEffect, pc.PostEffect); RefractionPostEffect.prototype = pc.extend (RefractionPostEffect.prototype, // Hver posteffekt må implementere gjengivelsen metode som // angir parametere som skyggeren kan kreve og / / gjør også effekten på skjermen gjengivelse: funksjon (inputTarget, outputTarget, rect) var enhet = this.device; var scope = device.scope; // Set th e input gir mål til shader. Dette er bildet gjengitt fra kameraets scope.resolve ("uColorBuffer"). SetValue (inputTarget.colorBuffer); // Tegn en fullskjerm quad på utdata målet. I dette tilfellet er utgangsmålet skjermen. // Tegning en fullskjerm quad vil kjøre skyggen som vi definerte over pc.drawFullscreenQuad (enhet, outputTarget, this.vertexBuffer, this.shader, rect); ); return RefractionPostEffect: RefractionPostEffect; ()); // --------------- SCRIPT DEFINITION ------------------------ // var Refraction = pc. createScript ( 'refraksjon'); Refraction.attributes.add ('vs', type: 'asset', assetType: 'shader', tittel: 'Vertex Shader'); Refraction.attributes.add ('fs', type: 'asset', assetType: 'shader', tittel: 'Fragment Shader'); // initialiser kode kalt en gang per enhet Refraction.prototype.initialize = function () var effect = ny pc.RefractionPostEffect (this.app.graphicsDevice, this.vs.resource, this.fs.resource); // legg til effekten til kameraets innleggEffekter kø var kø = this.entity.camera.postEffects; queue.addEffect (effekt); this.effect = effect; // Lagre gjeldende shaders for hot reload this.savedVS = this.vs.resource; this.savedFS = this.fs.resource; ; Refraction.prototype.update = function () if (this.savedFS! = This.fs.resource || this.savedVS! = This.vs.resource) this.swap (this); ; Refraction.prototype.swap = funksjon (gammel) this.entity.camera.postEffects.removeEffect (old.effect); this.initialize (); ;
Dette er akkurat som et vanlig skript, men vi definerer en RefractionPostEffect
klasse som kan brukes på kameraet. Dette trenger et vertex og en fragmentarisk skygger å gjengi. Attributtene er allerede satt opp, så la oss lage Refraction.frag med dette innholdet:
presisjon highp float; enhetlig sampler2D uColorBuffer; varierende vec2 vUv0; void main () vec4 color = texture2D (uColorBuffer, vUv0); gl_FragColor = farge;
Og Refraction.vert med en grunnleggende vertex shader:
attributt vec2 aPosition; varierende vec2 vUv0; void main (void) gl_Position = vec4 (aPosition, 0,0, 1,0); vUv0 = (aPosition.xy + 1.0) * 0,5;
Fest nå Refraction.js skript til kameraet, og tilordne shaders til de aktuelle attributter. Når du starter spillet, bør du se scenen akkurat som det var før. Dette er en blank innleggseffekt som ganske enkelt gjenoppretter scenen. For å verifisere at dette virker, prøv å gi scenen en rød fargetone.
I Refraction.frag, i stedet for å bare returnere fargen, prøv å sette den røde komponenten til 1,0, som skal se ut som bildet nedenfor.
Vi må legge til en tidsuniform for den animerte forvrengningen, så fortsett og opprett en i Refraction.js, inne i denne konstruktøren for ettervirkningen:
var RefractionPostEffect = funksjon (graphicsDevice, vs, fs) var fragmentShader = "presisjon" + graphicsDevice.precision + "float; \ n"; fragmentShader = fragmentShader + fs; // dette er skyggedefinisjonen for effekten vår this.shader = ny pc.Shader (graphicsDevice, attributes: aPosition: pc.SEMANTIC_POSITION, vshader: vs, fshader: fs); // >>>>>>>>>>>>> Initialiser tiden her this.time = 0; ;
Nå, innenfor denne gjengivelsesfunksjonen, sender vi den til vår skygge og øker den:
RefractionPostEffect.prototype = pc.extend (RefractionPostEffect.prototype, // Hver posteffekt må implementere gjengivelsesmetoden som // angir noen parametere som shader kan kreve og / / gjør også effekten på skjermen gjengivelse: funksjon (inputTarget, outputTarget, rect) var enhet = this.device; var scope = device.scope; // Sett innføringsgjenstandsmålet til shader. Dette er bildet gjengitt fra vårt kamera scope.resolve ("uColorBuffer"). setValue (inputTarget .colorBuffer); /// >>>>>>>>>>> Pass på tiden uniform her her scope.resolve ("uTime"). setValue (this.time); this.time + = 0,1; // Tegne en full skjerm quad på utdata målet. I dette tilfellet er utdata målet skjermen. // Tegning en fullskjerm quad vil kjøre skyggen som vi definerte ovenfor pc.drawFullscreenQuad (enhet, outputTarget, dette. vertexBuffer, this.shader, rect););
Nå kan vi bruke den samme skyggekoden fra vannforvrengningstutorialen, slik at vårt fulle fragment shader ser slik ut:
presisjon highp float; enhetlig sampler2D uColorBuffer; ensartet flyte deg; varierende vec2 vUv0; void main () vec2 pos = vUv0; float X = pos.x * 15. + uTime * 0.5; float Y = pos.y * 15. + uTime * 0.5; pos.y + = cos (X + Y) * 0,01 * cos (Y); pos.x + = synd (X-Y) * 0,01 * sin (Y); vec4 color = texture2D (uColorBuffer, pos); gl_FragColor = farge;
Hvis alt trengs, skal alt se ut som om det er under vann, som nedenfor.
Utfordring # 1: Forvrengningen gjelder kun nederst på skjermen.
Vi er nesten der. Alt vi trenger å gjøre nå er å bruke denne forvrengningseffekten bare på undersiden av skjermen. Den enkleste måten jeg har kommet på med å gjøre dette er å gjenopprette scenen med vannoverflaten som er gjengitt som en solid hvit, som vist nedenfor.
Dette ville bli gjengitt til en tekstur som ville fungere som en maske. Vi ville da passere denne tekstur til vår refraksjonsskygge, som bare ville forvride en piksel i det endelige bildet hvis den tilsvarende piksel i masken er hvit.
La oss legge til en boolsk egenskap på vannoverflaten for å vite om den brukes som en maske. Legg dette til Water.js:
Water.attributes.add ('isMask', type: 'boolean', tittel: "Er Mask?");
Vi kan så sende det til shader med material.setParameter ( 'isMask', this.isMask);
som vanlig. Deretter erklæres det i Water.frag og sett fargen til hvit hvis det er sant.
// Erklære den nye uniformen på den øverste unike bollen isMask; // På slutten av hovedfunksjonen, overstyr fargen som skal være hvit // hvis masken er sant hvis (isMask) color = vec4 (1.0);
Bekreft at dette fungerer ved å bytte "Er masken?" eiendom i redaktøren og gjenoppstart av spillet. Det skal se hvitt, som i det tidligere bildet.
Nå, for å gjenopprette scenen, trenger vi et andre kamera. Opprett et nytt kamera i redigeringsprogrammet og ring det CameraMask. Dupliser Water-enheten i redaktøren også, og ring den WaterMask. Pass på at "Er maske?" er falsk for vann-enheten, men sant for WaterMask.
For å fortelle det nye kameraet å gjengi en tekstur i stedet for skjermen, opprett et nytt skript som heter CameraMask.js og fest den til det nye kameraet. Vi lager en RenderTarget for å fange dette kameraets utgang slik:
// initialiser kode kalt en gang per enhet CameraMask.prototype.initialize = function () // Lag et 512x512x24-bit gjengemål med en dybdebuffer var fargeBuffer = ny pc.Texture (this.app.graphicsDevice, width: 512, høyde: 512, format: pc.PIXELFORMAT_R8_G8_B8, autoMipmap: true); colorBuffer.minFilter = pc.FILTER_LINEAR; colorBuffer.magFilter = pc.FILTER_LINEAR; var renderTarget = ny pc.RenderTarget (this.app.graphicsDevice, colorBuffer, dybde: true); this.entity.camera.renderTarget = renderTarget; ;
Nå, hvis du starter, ser du at dette kameraet ikke lenger gjengis til skjermen. Vi kan ta tak i produksjonen av det gjengitte målet i Refraction.js som dette:
Refraction.prototype.initialize = function () var cameraMask = this.app.root.findByName ('CameraMask'); var maskBuffer = cameraMask.camera.renderTarget.colorBuffer; var effekt = ny pc.RefractionPostEffect (this.app.graphicsDevice, this.vs.resource, this.fs.resource, maskBuffer); // ... // Resten av denne funksjonen er den samme som før ►;
Legg merke til at jeg sender denne maskestrukturen som et argument til post-effektkonstruktøren. Vi må opprette en referanse til det i vår konstruktør, slik at det ser ut som:
//// Lagt til et ekstra argument på linjen under var RefractionPostEffect = funksjon (graphicsDevice, vs, fs, buffer) var fragmentShader = "precision" + graphicsDevice.precision + "float; \ n"; fragmentShader = fragmentShader + fs; // dette er skyggedefinisjonen for effekten vår this.shader = ny pc.Shader (graphicsDevice, attributes: aPosition: pc.SEMANTIC_POSITION, vshader: vs, fshader: fs); this.time = 0; //// <<<<<<<<<<<<< Saving the buffer here this.buffer = buffer; ;
Til slutt, i gjengivelsesfunksjonen, send bufferen til vår skygge med:
scope.resolve ( "uMaskBuffer") SetValue (this.buffer.);
Nå for å bekrefte at alt dette fungerer, la jeg det som en utfordring.
Utfordring # 2: Gi uMaskBuffer til skjermen for å bekrefte at det er utgang fra det andre kameraet.
En ting å være oppmerksom på er at gjengemålet er satt opp i initialiseringen av CameraMask.js, og det må være klart når Refraction.js kalles. Hvis skriptene kjører omvendt, får du en feil. For å sikre at de kjører i riktig rekkefølge, drar du CameraMask øverst i entitetslisten i redigeringsprogrammet, som vist nedenfor.
Det andre kameraet skal alltid se på samme visning som den opprinnelige, så la oss få det til å alltid følge sin posisjon og rotasjon i oppdateringen av CameraMask.js:
CameraMask.prototype.update = function (dt) var pos = this.CameraToFollow.getPosition (); var rot = this.CameraToFollow.getRotation (); this.entity.setPosition (pos.x, pos.y, pos.z); this.entity.setRotation (rot); ;
Og definer CameraToFollow
i initialiseringen:
this.CameraToFollow = this.app.root.findByName ('Camera');
Begge kameraene gjør for øyeblikket det samme. Vi vil at maskekameraet skal gjengi alt unntatt det virkelige vannet, og vi vil at det virkelige kameraet skal gjengi alt unntatt maskevannet.
For å gjøre dette kan vi bruke kameraets culling bitmask. Dette fungerer på samme måte som kollisionsmasker hvis du noen gang har brukt dem. Et objekt blir slettet (ikke gjengitt) hvis resultatet av bitvis OG
mellom masken og kameramasken er 1.
La oss si at vannet vil ha bit 2 sett, og WaterMask vil ha bit 3. Deretter må det virkelige kameraet ha alle biter sett unntatt 3, og maske kameraet må ha alle biter satt bortsett fra 2. En enkel måte å si "alle biter unntatt N" er å gjøre:
~ (1 << N) >>> 0
Du kan lese mer om bitwise operatører her.
For å sette opp kollasjemaskene, kan vi sette dette innvendig CameraMask.js's initialiserer på bunnen:
// Sett alle biter bortsett fra 2 this.entity.camera.camera.cullingMask & = ~ (1 << 2) >>> 0; // Sett alle biter unntatt 3 this.CameraToFollow.camera.camera.cullingMask & = ~ (1 << 3) >>> 0; // Hvis du vil skrive ut denne bitmasken, prøv: // console.log ((this.CameraToFollow.camera.camera.cullingMask >>> 0) .toString (2));
Nå, i Water.js, sett vannmaskens maske på bit 2, og maskeversjonen av den på bit 3:
// Sett dette nederst på initialiseringen av Water.js // Sett dekkmasker var bit = this.isMask? 3: 2; meshInstance.mask = 0; meshInstance.mask | = (1 << bit);
Nå vil en visning ha det vanlige vannet, og det andre vil ha det faste hvite vannet. Den venstre halvdelen av bildet nedenfor er utsikten fra det opprinnelige kameraet, og den høyre halvdelen er fra maskekameraet.
Ett siste skritt nå! Vi vet at områdene under vann er merket med hvite piksler. Vi trenger bare å sjekke om vi ikke er på en hvit piksel, og i så fall, slå av forvrengningen i Refraction.frag:
// Kontroller originalposisjonen samt ny forvrengt posisjon vec4 maskColor = texture2D (uMaskBuffer, pos); vec4 maskColor2 = texture2D (uMaskBuffer, vUv0); // Vi er ikke på en hvit piksel? hvis (maskColor! = vec4 (1.0) || maskColor2! = vec4 (1.0)) // Returner den tilbake til den opprinnelige posisjonen pos = vUv0;
Og det burde gjøre det!
En ting å merke seg er at siden tekstur til masken initialiseres ved lansering, hvis du endrer størrelsen på vinduet ved kjøring, vil det ikke lenger matche skjermstørrelsen.
Som et valgfritt oppryddingstrinn, har du kanskje lagt merke til at kantene i scenen nå ser litt skarpe ut. Dette skyldes at når vi brukte vår ettervirkning, mistet vi anti-aliasing.
Vi kan søke et ekstra anti-alias utover effekten vår som en annen ettervirkning. Heldigvis finnes det en tilgjengelig i PlayCanvas-butikken vi bare kan bruke. Gå til skriptets aktivitetsside, klikk på den store grønne nedlastningsknappen, og velg prosjektet fra listen som vises. Skriptet vil vises i roten av aktivitetsvinduet som posteffect-fxaa.js. Bare legg dette til Camera-enheten, og scenen din skal se litt finere ut!
Hvis du har gjort det så langt, gi deg selv et klapp på ryggen! Vi dekket mange teknikker i denne serien. Du bør nå være komfortabel med vertex shaders, gjengivelse av teksturer, søke etterbehandlingseffekter, selektivt kaste gjenstander, bruke dybdebufferen, og arbeide med blanding og gjennomsiktighet. Selv om vi implementerte dette i PlayCanvas, er disse alle generelle grafikkonseptene du finner i noen form på hvilken plattform du ender i.
Alle disse teknikkene gjelder også for en rekke andre effekter. En spesielt interessant søknad jeg har funnet på vertex shaders er i denne snakken på Abhes kunst, hvor de forklarer hvordan de brukte vertex shaders for å effektivt animere titusenvis av fisk på skjermen.
Du bør nå også ha en fin vann effekt du kan søke på spillene dine! Du kan enkelt tilpasse det nå, siden du har satt sammen hver eneste detalj. Det er fortsatt mye mer du kan gjøre med vann (jeg har ikke engang nevnt noen form for refleksjon i det hele tatt). Nedenfor er et par ideer.
I stedet for å bare animere bølgene med en kombinasjon av sinus og cosinus, kan du prøve en støytekstur for å få bølgene til å se litt mer naturlig og uforutsigbar.
I stedet for helt statiske vannlinjer på overflaten, kan du trekke på den tekstur når objekter beveger seg, for å skape et dynamisk skumspor. Det er mange måter å gjøre ved å gjøre dette, så dette kan være sitt eget prosjekt.
Du finner det ferdige vertsbaserte PlayCanvas-prosjektet her. En Three.js-port er også tilgjengelig i dette depotet.