Welkom terug in deze driedelige serie over het maken van gestileerd water van toon in PlayCanvas met behulp van vertex shaders. In deel 2 hebben we drijfvermogen en schuimlijnen behandeld. In dit laatste deel gaan we de onderwatervervorming toepassen als een post-proceseffect.
Ons doel is om de breking van licht visueel over water te communiceren. We hebben al besproken hoe je dit soort vervorming in een fragment-shader kunt maken in een eerdere zelfstudie voor een 2D-scène. Het enige verschil hier is dat we moeten uitzoeken welk deel van het scherm onder water is en alleen de vervorming daar toepassen.
Over het algemeen wordt een postprocedé-effect toegepast op de hele scène nadat deze is gerenderd, zoals een gekleurde tint of een oud CRT-schermeffect. In plaats van je scène direct op het scherm weer te geven, render je deze eerst naar een buffer of textuur en render je die vervolgens naar het scherm, waarbij je een aangepaste shader passeert.
In PlayCanvas kunt u een post-proceseffect instellen door een nieuw script te maken. Noem het Refraction.js, en kopieer deze sjabloon om te beginnen met:
// --------------- EFFECTIE DEFINITIE ------------------------ // pc.extend ( pc, function () // Constructor - Creëert een exemplaar van onze functie post var var RefractionPostEffect = (graphicsDevice, vs, fs, buffer) var fragmentShader = "precision" + graphicsDevice.precision + "float; \ n"; fragmentShader = fragmentShader + fs; // dit is de shader-definitie voor ons effect this.shader = nieuwe pc.Shader (graphicsDevice, attributes: aPosition: pc.SEMANTIC_POSITION, vshader: vs, fshader: fs); this.buffer = buffer;; // Ons effect moet afgeleid zijn van pc.PostEffect RefractionPostEffect = pc.inherits (RefractionPostEffect, pc.PostEffect); RefractionPostEffect.prototype = pc.extend (RefractionPostEffect.prototype, // Elk berichteffect moet de render implementeren methode die // alle parameters instelt die de arcering kan vereisen en // ook het effect op de render render rendert: function (inputTarget, outputTarget, rect) var device = this.device; var scope = device.scope; // Set th e invoer renderdoel naar de arcering. Dit is de afbeelding van onze camera scope.resolve ("uColorBuffer"). SetValue (inputTarget.colorBuffer); // Teken een quad op volledig scherm op het uitvoerdoel. In dit geval is het uitvoerdoel het scherm. // Als u een quad op volledig scherm tekent, wordt de arcering uitgevoerd die we hierboven hebben gedefinieerd pc.drawFullscreenQuad (device, outputTarget, this.vertexBuffer, this.shader, rect); ); return RefractionPostEffect: RefractionPostEffect; ()); // --------------- SCRIPT DEFINITION ------------------------ // var Refraction = pc. createScript (breking); Refraction.attributes.add ('vs', type: 'asset', assetType: 'shader', title: 'Vertex Shader'); Refraction.attributes.add ('fs', type: 'asset', assetType: 'shader', title: 'Fragment Shader'); // initialiseer de code eenmaal per entiteit genaamd Refraction.prototype.initialize = function () var effect = new pc.RefractionPostEffect (this.app.graphicsDevice, this.vs.resource, this.fs.resource); // voeg het effect toe aan de post van de camera. Effecten wachtrij var queue = this.entity.camera.postEffects; queue.addEffect (effect); this.effect = effect; // Sla de huidige shaders op voor 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 = function (oud) this.entity.camera.postEffects.removeEffect (old.effect); this.initialize (); ;
Dit is net een normaal script, maar we definiëren een RefractionPostEffect
klasse die op de camera kan worden toegepast. Dit heeft een hoekpunt en een fragmentshader nodig om te renderen. De kenmerken zijn al ingesteld, dus laten we maken Refraction.frag met deze inhoud:
precisie highp vlotter; uniforme sampler2D uColorBuffer; variërende vec2 vUv0; void main () vec4 color = texture2D (uColorBuffer, vUv0); gl_FragColor = kleur;
En Refraction.vert met een standaard vertex-arcering:
attribuut vec2 aPosition; variërende vec2 vUv0; void main (void) gl_Position = vec4 (aPosition, 0.0, 1.0); vUv0 = (aPosition.xy + 1.0) * 0.5;
Bevestig nu de Refraction.js script naar de camera en wijs de shaders toe aan de juiste kenmerken. Wanneer u het spel start, zou u de scène precies moeten zien zoals eerder. Dit is een leeg post-effect dat de scène gewoon opnieuw rendert. Om te controleren of dit werkt, probeert u de scène een rode tint te geven.
Zet in Refraction.frag in plaats van alleen de kleur terug te zetten de rode component in op 1.0, wat er uit zou moeten zien als de afbeelding hieronder.
We moeten een tijduniform toevoegen voor de geanimeerde vervorming, dus ga je gang en maak er een in Refraction.js, in deze constructor voor het post-effect:
var RefractionPostEffect = function (graphicsDevice, vs, fs) var fragmentShader = "precision" + graphicsDevice.precision + "float; \ n"; fragmentShader = fragmentShader + fs; // dit is de shader-definitie voor ons effect this.shader = nieuwe pc.Shader (graphicsDevice, attributes: aPosition: pc.SEMANTIC_POSITION, vshader: vs, fshader: fs); // >>>>>>>>>>>>> Initialiseer de tijd hier this.time = 0; ;
In deze renderfunctie geven we deze nu door aan onze arcering en verhogen deze:
RefractionPostEffect.prototype = pc.extend (RefractionPostEffect.prototype, // Elk post-effect moet de weergavemethode implementeren die // parameters instelt die de arcering kan vereisen en // geeft ook het effect op de functie render renderen: (inputTarget, outputTarget, rect) var device = this.device; var scope = device.scope; // Stel het invoerrenderingsdoel in op de arcering Dit is de afbeelding die wordt weergegeven vanuit onze camera scope.resolve ("uColorBuffer"). setValue (inputTarget .colorBuffer); /// >>>>>>>>>>>>>> Geef de tijd uniform hier scope.resolve ("uTime"). setValue (this.time); this.time + = 0.1; // Teken een quad op volledig scherm op het uitvoerdoel.In dit geval is het uitvoerdoel het scherm // Een volledig scherm quad tekenen zal de shader uitvoeren die we hierboven hebben gedefinieerd pc.drawFullscreenQuad (device, outputTarget, this. vertexBuffer, this.shader, rect););
Nu kunnen we dezelfde shadercode gebruiken in de zelfstudie voor watervervorming, waardoor onze volledige fragmentshader er zo uitziet:
precisie highp vlotter; uniforme sampler2D uColorBuffer; uniforme float uTime; variërende vec2 vUv0; void main () vec2 pos = vUv0; zweven X = pos.x * 15. + uTijd * 0,5; zweven Y = pos.y * 15. + uTime * 0,5; pos.y + = cos (X + Y) * 0.01 * cos (Y); pos.x + = sin (X-Y) * 0.01 * sin (Y); vec4 color = texture2D (uColorBuffer, pos); gl_FragColor = kleur;
Als alles goed was gegaan, zou alles er nu uit moeten zien alsof het onder water is, zoals hieronder.
Uitdaging # 1: maak de vervorming alleen van toepassing op de onderste helft van het scherm.
We zijn er bijna. Het enige wat we nu hoeven te doen is dit vervormingseffect alleen op het onderwatergedeelte van het scherm toepassen. De meest voor de hand liggende manier waarop ik dit heb bedacht is om de scène opnieuw te renderen met het wateroppervlak weergegeven als een effen wit, zoals hieronder weergegeven.
Dit zou worden weergegeven in een structuur die als een masker zou fungeren. We zouden deze textuur vervolgens doorgeven aan onze brekingshader, die een pixel in het uiteindelijke beeld alleen zou vervormen als de overeenkomstige pixel in het masker wit is.
Laten we een booleaans attribuut op het wateroppervlak toevoegen om te weten of het als een masker wordt gebruikt. Voeg dit toe aan Water.js:
Water.attributes.add ('isMask', type: 'boolean', title: "Is Mask?");
We kunnen het dan doorgeven aan de arcering met material.setParameter (isMask ', this.isMask);
zoals gewoonlijk. Verklaar het vervolgens in Water.frag en stel de kleur in op wit als het waar is.
// Declareer het nieuwe uniform aan de bovenste uniforme bool isMask; // Aan het einde van de hoofdfunctie overschrijft u de kleur als wit // als het masker waar is als (isMask) color = vec4 (1.0);
Bevestig dat dit werkt door de "Is masker?" eigendom in de editor en het spel opnieuw starten. Het moet er wit uitzien, zoals in de eerdere afbeelding.
Om de scène opnieuw te renderen, hebben we nu een tweede camera nodig. Maak een nieuwe camera in de editor en bel hem CameraMask. Dupliceer ook de Water-entiteit in de editor en noem die WaterMask. Zorg ervoor dat het "Is masker?" is niet waar voor Water, maar is waar voor WaterMask.
Als u de nieuwe camera wilt laten renderen naar een structuur in plaats van naar het scherm, maakt u een nieuw script met de naam CameraMask.js en bevestig deze aan de nieuwe camera. We maken een RenderTarget om de uitvoer van deze camera als volgt vast te leggen:
// initialiseer code eenmaal per entiteit genaamd CameraMask.prototype.initialize = function () // Maak een 512x512x24-bit renderdoel met een dieptebuffer var colorBuffer = new pc.Texture (this.app.graphicsDevice, width: 512, hoogte: 512, indeling: pc.PIXELFORMAT_R8_G8_B8, autoMipmap: true); colorBuffer.minFilter = pc.FILTER_LINEAR; colorBuffer.magFilter = pc.FILTER_LINEAR; var renderTarget = nieuwe pc.RenderTarget (this.app.graphicsDevice, colorBuffer, depth: true); this.entity.camera.renderTarget = renderTarget; ;
Als u nu start, ziet u dat deze camera niet langer wordt weergegeven op het scherm. We kunnen de uitvoer van zijn renderdoel ophalen in Refraction.js zoals dit:
Refraction.prototype.initialize = function () var cameraMask = this.app.root.findByName ('CameraMask'); var maskBuffer = cameraMask.camera.renderTarget.colorBuffer; var effect = nieuwe pc.RefractionPostEffect (this.app.graphicsDevice, this.vs.resource, this.fs.resource, maskBuffer); // ... // De rest van deze functie is hetzelfde als eerder;
Merk op dat ik deze maskertextuur doorgeef aan de post-effectconstructor. We moeten er een verwijzing naar maken in onze constructor, dus het ziet eruit als:
//// Een extra argument toegevoegd op de regel onder var RefractionPostEffect = function (graphicsDevice, vs, fs, buffer) var fragmentShader = "precision" + graphicsDevice.precision + "float; \ n"; fragmentShader = fragmentShader + fs; // dit is de shader-definitie voor ons effect this.shader = nieuwe pc.Shader (graphicsDevice, attributes: aPosition: pc.SEMANTIC_POSITION, vshader: vs, fshader: fs); this.time = 0; //// <<<<<<<<<<<<< Saving the buffer here this.buffer = buffer; ;
Tenslotte, geef in de renderfunctie de buffer door aan onze shader met:
scope.resolve ( "uMaskBuffer") setValue (this.buffer).;
Nu om te verifiëren dat dit allemaal werkt, zal ik dat als een uitdaging verlaten.
Uitdaging # 2: Render de uMaskBuffer naar het scherm om te bevestigen dat dit de uitvoer is van de tweede camera.
Een ding om op te letten is dat het renderdoel is ingesteld in de initialisatie van CameraMask.js, en dat moet klaar zijn op het moment dat Refraction.js wordt genoemd. Als de scripts de andere kant op lopen, krijgt u een foutmelding. Om ervoor te zorgen dat ze in de juiste volgorde worden uitgevoerd, sleept u het Cameramas naar de bovenkant van de entiteitenlijst in de editor, zoals hieronder wordt weergegeven.
De tweede camera zou altijd dezelfde weergave moeten hebben als de originele, dus laten we ervoor zorgen dat het altijd zijn positie en rotatie volgt in de update van 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); ;
En definieer CameraToFollow
in de initialisatie:
this.CameraToFollow = this.app.root.findByName ('Camera');
Beide camera's geven momenteel hetzelfde weer. We willen dat de maskercamera alles weergeeft behalve het echte water en we willen dat de echte camera alles weergeeft behalve het maskerwater.
Om dit te doen, kunnen we het afkapbitmasker van de camera gebruiken. Dit werkt op dezelfde manier als botsmaskers als je die ooit hebt gebruikt. Een object wordt geruimd (niet weergegeven) als het resultaat van een bitwise is EN
tussen het masker en het cameramasker is 1.
Laten we zeggen dat het water bit 2 heeft ingesteld en WaterMask bit 3. Dan moet de echte camera alle bits hebben, behalve 3, en moet de maskercamera alle bits hebben ingesteld behalve 2. Een eenvoudige manier om te zeggen "alle bits behalve N" is om te doen:
~ (1 << N) >>> 0
U kunt hier meer lezen over bitwise-operators.
Om de maskers voor het ruimen van de camera in te stellen, kunnen we dit in de camera plaatsen CameraMask.js's initialiseren onderaan:
// Zet alle bits behalve 2 this.entity.camera.camera.cullingMask & = ~ (1 << 2) >>> 0; // Zet alle bits behalve deze op 3.CameraToFollow.camera.camera.cullingMask & = ~ (1 << 3) >>> 0; // Als u dit bitmasker wilt afdrukken, probeer dan: // console.log ((this.CameraToFollow.camera.camera.cullingMask >>> 0) .toString (2));
Stel nu in Water.js het masker van het waternetwerk in op bit 2 en de maskerversie ervan op bit 3:
// Zet dit onderaan de initialisatie van Water.js // Stel de ruimingsmaskers in var bit = this.isMask? 3: 2; meshInstance.mask = 0; meshInstance.mask | = (1 << bit);
Nu zal één weergave het normale water hebben en de andere het vaste witte water. De linkerhelft van de afbeelding hieronder is het beeld van de originele camera en de rechter helft is van de maskercamera.
Een laatste stap nu! We weten dat de gebieden onder water zijn gemarkeerd met witte pixels. We moeten alleen controleren of we geen witte pixel hebben en, als dit het geval is, de vervorming uitschakelen Refraction.frag:
// Controleer de oorspronkelijke positie en de nieuwe vervormde positie vec4 maskColor = texture2D (uMaskBuffer, pos); vec4 maskColor2 = texture2D (uMaskBuffer, vUv0); // We zitten niet op een witte pixel? if (maskColor! = vec4 (1.0) || maskColor2! = vec4 (1.0)) // Zet het terug naar de oorspronkelijke positie pos = vUv0;
En dat zou het moeten doen!
Een ding om op te merken is dat, aangezien de textuur voor het masker wordt geïnitialiseerd bij het starten, als u tijdens runtime het formaat van het venster aanpast, dit niet langer overeenkomt met de grootte van het scherm.
Als een optionele opschoonstap is het je misschien opgevallen dat de randen in de scène er nu een beetje scherp uitzien. Dit komt omdat toen we ons post-effect toepasten, we anti-aliasing verloren.
We kunnen een extra anti-alias bovenop ons effect aanbrengen als een ander post-effect. Gelukkig is er een beschikbaar in de PlayCanvas-winkel die we gewoon kunnen gebruiken. Ga naar de scriptactivapagina, klik op de grote groene downloadknop en kies uw project uit de lijst die wordt weergegeven. Het script verschijnt in de hoofdmap van uw activatievenster posteffect-fxaa.js. Bevestig dit gewoon aan de camera-entiteit en uw scène moet er iets leuker uitzien!
Als je zover bent gekomen, geef jezelf een schouderklopje! We hebben veel technieken in deze serie behandeld. U moet zich nu op uw gemak voelen met vertex shaders, rendering naar texturen, toepassen van nabewerkingseffecten, selectief verwijderen van objecten, gebruik maken van de dieptebuffer en werken met blending en transparantie. Hoewel we dit in PlayCanvas implementeerden, zijn dit allemaal algemene grafische concepten die je op een of andere manier kunt vinden op welk platform je ook terechtkomt.
Al deze technieken zijn ook van toepassing op een verscheidenheid aan andere effecten. Een bijzonder interessante toepassing die ik van vertex shaders heb gevonden, is in deze lezing over de kunst van Abzu, waar ze uitleggen hoe ze vertex shaders gebruikten om tienduizenden vissen efficiënt op het scherm te animeren.
Je zou nu ook een mooi watereffect moeten hebben dat je op je spellen kunt toepassen! Je zou het gemakkelijk kunnen aanpassen nu je elk detail zelf hebt samengesteld. Er is nog veel meer dat je kunt doen met water (ik heb zelfs helemaal geen reflectie genoemd). Hieronder enkele ideeën.
In plaats van alleen de golven te animeren met een combinatie van sinus en cosinussen, kunt u een ruisstructuur samplen om de golven er natuurlijker en onvoorspelbaarder uit te laten zien.
In plaats van volledig statische waterlijnen op het oppervlak, kunt u op die textuur tekenen wanneer objecten bewegen, om een dynamisch schuimpad te creëren. Er zijn veel manieren om dit te doen, dus dit kan zijn eigen project zijn.
Je kunt het voltooide gehoste PlayCanvas-project hier vinden. Een Three.js-poort is ook beschikbaar in deze repository.