สร้างเฉดสีด้วย Babylon.js

เผยแพร่แล้ว: 2022-03-10
สรุปโดยย่อ ↬ Shaders เป็นแนวคิดหลักหากคุณต้องการปลดปล่อยพลังดิบของ GPU ของคุณ ฉันจะช่วยให้คุณเข้าใจวิธีการทำงานและแม้กระทั่งทดลองพลังภายในด้วยวิธีง่ายๆ ต้องขอบคุณ Babylon.js ก่อนทำการทดลอง เราต้องดูก่อนว่าสิ่งต่าง ๆ ทำงานภายในอย่างไร เมื่อต้องรับมือกับ 3D ที่เร่งด้วยฮาร์ดแวร์ คุณจะต้องจัดการกับซีพียูสองตัว: CPU หลักและ GPU GPU เป็น CPU ชนิดหนึ่งที่เชี่ยวชาญเป็นพิเศษ

Shaders เป็นแนวคิดหลักหากคุณต้องการปลดปล่อยพลังดิบของ GPU ของคุณ ฉันจะช่วยให้คุณเข้าใจวิธีการทำงานและแม้กระทั่งทดลองพลังภายในด้วยวิธีง่ายๆ ต้องขอบคุณ Babylon.js

มันทำงานอย่างไร?

ก่อนทำการทดลอง เราต้องดูก่อนว่าสิ่งต่าง ๆ ทำงานภายในอย่างไร

เมื่อต้องรับมือกับ 3D ที่เร่งด้วยฮาร์ดแวร์ คุณจะต้องจัดการกับซีพียูสองตัว: CPU หลักและ GPU GPU เป็น CPU ชนิดหนึ่งที่เชี่ยวชาญเป็นพิเศษ

อ่านเพิ่มเติม เกี่ยวกับ SmashingMag:

  • สร้างเกม WebGL ข้ามแพลตฟอร์มด้วย Babylon.js
  • การใช้ Gamepad API ในเว็บเกม
  • บทนำสู่การสร้างแบบจำลองเหลี่ยมและ Three.js
  • วิธีสร้างเครื่องดรัม 8 บิตที่ตอบสนองได้ดี
เพิ่มเติมหลังกระโดด! อ่านต่อด้านล่าง↓

GPU เป็นเครื่องสถานะที่คุณตั้งค่าโดยใช้ CPU ตัวอย่างเช่น CPU จะกำหนดค่า GPU ให้แสดงเส้นแทนที่จะเป็นรูปสามเหลี่ยม มันจะกำหนดว่าโปร่งใสเปิดอยู่หรือไม่ และอื่นๆ

เมื่อตั้งค่าสถานะทั้งหมดแล้ว CPU สามารถกำหนดสิ่งที่จะแสดง: เรขาคณิต

เรขาคณิตประกอบด้วย:

  • รายการจุดที่เรียกว่าจุดยอดและเก็บไว้ในอาร์เรย์ที่เรียกว่าบัฟเฟอร์จุดยอด
  • รายการดัชนีที่กำหนดใบหน้า (หรือสามเหลี่ยม) ที่จัดเก็บไว้ในอาร์เรย์ชื่อบัฟเฟอร์ดัชนี

ขั้นตอนสุดท้ายสำหรับ CPU คือการกำหนดวิธีการแสดงเรขาคณิต สำหรับงานนี้ CPU จะกำหนดเฉดสีใน GPU Shaders เป็นโค้ดบางส่วนที่ GPU จะดำเนินการสำหรับจุดยอดและพิกเซลแต่ละจุดที่จะแสดงผล (จุดยอด - หรือจุดยอดเมื่อมีหลายจุด - เป็น "จุด" ในแบบ 3 มิติ)

เฉดสีมีสองประเภท: เฉดสีจุดยอดและเฉดสีพิกเซล (หรือส่วนย่อย)

ท่อกราฟิก

ก่อนจะไปเจาะลึกเชดเดอร์ ให้ถอยออกมาก่อน ในการเรนเดอร์พิกเซล GPU จะใช้เรขาคณิตที่กำหนดโดย CPU และจะทำสิ่งต่อไปนี้:

  • การใช้บัฟเฟอร์ดัชนี จะรวบรวมจุดยอดสามจุดเพื่อกำหนดสามเหลี่ยม
  • บัฟเฟอร์ดัชนีประกอบด้วยรายการดัชนีจุดยอด ซึ่งหมายความว่าแต่ละรายการในบัฟเฟอร์ดัชนีคือจำนวนของจุดยอดในบัฟเฟอร์จุดยอด
  • สิ่งนี้มีประโยชน์มากเพื่อหลีกเลี่ยงจุดยอดที่ซ้ำกัน

ตัวอย่างเช่น บัฟเฟอร์ดัชนีต่อไปนี้เป็นรายการของสองหน้า: [1 2 3 1 3 4] ใบหน้าแรกประกอบด้วยจุดยอด 1 จุดยอด 2 และจุดยอด 3 ใบหน้าที่สองประกอบด้วยจุดยอด 1 จุดยอด 3 และจุดยอด 4 ดังนั้น มีจุดยอดสี่จุดในเรขาคณิตนี้:

(ดูรุ่นใหญ่)

จุดยอดจะใช้กับจุดยอดแต่ละจุดของสามเหลี่ยม เป้าหมายหลักของจุดยอดคือการสร้างพิกเซลสำหรับแต่ละจุดยอด (การฉายภาพบนหน้าจอ 2 มิติของจุดยอด 3 มิติ):

(ดูรุ่นใหญ่)

การใช้พิกเซลทั้งสามนี้ (ซึ่งกำหนดรูปสามเหลี่ยม 2 มิติบนหน้าจอ) GPU จะสอดแทรกค่าทั้งหมดที่แนบมากับพิกเซล (อย่างน้อยก็ตำแหน่ง) และพิกเซลเชดเดอร์จะถูกนำไปใช้กับทุกพิกเซลที่รวมอยู่ในสามเหลี่ยม 2 มิติเพื่อ สร้างสีให้กับทุกพิกเซล:

(ดูรุ่นใหญ่)

กระบวนการนี้เสร็จสิ้นสำหรับทุกๆ ใบหน้าที่กำหนดโดยบัฟเฟอร์ดัชนี

เห็นได้ชัดว่าเนื่องจากลักษณะคู่ขนาน GPU จึงสามารถประมวลผลขั้นตอนนี้สำหรับหลาย ๆ ใบหน้าพร้อมกันและบรรลุประสิทธิภาพที่ดีจริงๆ

GLSL

เราเพิ่งเห็นว่าในการเรนเดอร์สามเหลี่ยม GPU ต้องการตัวแรเงาสองตัว: ตัวแรเงาจุดยอดและตัวแรเงาพิกเซล เฉดสีเหล่านี้เขียนด้วยภาษาชื่อ Graphics Library Shader Language (GLSL) ดูเหมือนซี

นี่คือตัวอย่างของ shader เวอร์เท็กซ์ทั่วไป:

 precision highp float; // Attributes attribute vec3 position; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; }

โครงสร้าง Vertex Shader

Shader จุดยอดประกอบด้วยสิ่งต่อไปนี้:

  • คุณสมบัติ แอตทริบิวต์กำหนดส่วนของจุดยอด โดยค่าเริ่มต้น จุดยอดอย่างน้อยควรมีตำแหน่ง (a vector3:x, y, z ) อย่างไรก็ตาม ในฐานะนักพัฒนา คุณสามารถตัดสินใจเพิ่มข้อมูลเพิ่มเติมได้ ตัวอย่างเช่น ในอดีต shader มี vector2 ชื่อ uv (เช่น พิกัดพื้นผิวที่อนุญาตให้คุณใช้พื้นผิว 2D กับวัตถุ 3D)
  • ยูนิฟอร์ม . ชุดเครื่องแบบเป็นตัวแปรที่ใช้โดย shader และกำหนดโดย CPU ชุดเดียวที่เรามีคือเมทริกซ์ที่ใช้ฉายตำแหน่งของจุดยอด (x, y, z) ไปยังหน้าจอ (x, y)
  • ที่ แตกต่างกัน ตัวแปรที่แปรผันคือค่าที่สร้างโดยจุดยอดเชเดอร์และส่งไปยังพิกเซลเชดเดอร์ ที่นี่ vertex shader จะส่งค่า vUV (สำเนาอย่างง่ายของ uv ) ไปยัง pixel shader ซึ่งหมายความว่ามีการกำหนดพิกเซลที่นี่ด้วยพิกัดตำแหน่งและพื้นผิว ค่าเหล่านี้จะถูกสอดแทรกโดย GPU และใช้โดยตัวแบ่งพิกเซล
  • หลัก . ฟังก์ชันที่ชื่อ main คือโค้ดที่ GPU เรียกใช้สำหรับจุดยอดแต่ละจุด และอย่างน้อยต้องสร้างค่าสำหรับ gl_position (ตำแหน่งของจุดยอดปัจจุบันบนหน้าจอ)

เราจะเห็นได้ในตัวอย่างของเราว่าจุดยอดเชดเดอร์นั้นค่อนข้างเรียบง่าย มันสร้างตัวแปรระบบ (เริ่มต้นด้วย gl_ ) ชื่อ gl_position เพื่อกำหนดตำแหน่งของพิกเซลที่เกี่ยวข้อง และตั้งค่าตัวแปรที่แตกต่างกันที่เรียกว่า vUV

ลัทธิวูดูเบื้องหลังเมทริกซ์

สิ่งที่เกี่ยวกับเชเดอร์ของเราคือ เรามีเมทริกซ์ชื่อ worldViewProjection และเราใช้เมทริกซ์นี้เพื่อฉายตำแหน่งจุดยอดไปยังตัวแปร gl_position เยี่ยมเลย แต่เราจะหาค่าของเมทริกซ์นี้ได้อย่างไร? มันเป็นชุดเดียวกัน ดังนั้นเราจึงต้องกำหนดมันที่ฝั่ง CPU (โดยใช้ JavaScript)

นี่เป็นหนึ่งในส่วนที่ซับซ้อนของการทำ 3D คุณต้องเข้าใจคณิตศาสตร์ที่ซับซ้อน (หรือคุณจะต้องใช้เอ็นจิ้น 3 มิติ เช่น Babylon.js ซึ่งเราจะเห็นในภายหลัง)

เมทริกซ์ worldViewProjection คือการรวมกันของสามเมทริกซ์ที่แตกต่างกัน:

(ดูรุ่นใหญ่)

การใช้เมทริกซ์ผลลัพธ์ทำให้เราแปลงจุดยอด 3 มิติเป็นพิกเซล 2 มิติ โดยคำนึงถึงมุมมองและทุกสิ่งที่เกี่ยวข้องกับตำแหน่ง มาตราส่วน และการหมุนของวัตถุปัจจุบัน

นี่เป็นความรับผิดชอบของคุณในฐานะนักพัฒนา 3D: การสร้างและทำให้เมทริกซ์นี้เป็นข้อมูลล่าสุด

กลับไปที่ Shaders

เมื่อทำการเชเดอร์จุดยอดบนทุกจุดยอด (สามครั้ง จากนั้น) เราจะมีพิกเซลสามจุดที่มี gl_position ที่ถูกต้องและค่า vUV GPU จะสอดแทรกค่าเหล่านี้ในทุกพิกเซลที่อยู่ในรูปสามเหลี่ยมที่สร้างด้วยพิกเซลเหล่านี้

จากนั้น สำหรับแต่ละพิกเซล พิกเซลจะดำเนินการเชเดอร์พิกเซล:

 precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); }

โครงสร้าง Pixel (หรือ Fragment) Shader

โครงสร้างของพิกเซลเชดเดอร์นั้นคล้ายกับของเวอร์เท็กซ์เชเดอร์:

  • ที่ แตกต่างกัน ตัวแปรที่แตกต่างกันคือค่าที่สร้างโดยจุดยอดเชเดอร์และส่งไปยังตัวแก้ไขพิกเซล ที่นี่ Pixel shader จะได้รับค่า vUV จากจุดสุดยอด shader
  • ยูนิฟอร์ม . ชุดเครื่องแบบเป็นตัวแปรที่ใช้โดย shader และกำหนดโดย CPU ชุดเดียวที่เรามีคือ Sampler ซึ่งเป็นเครื่องมือที่ใช้ในการอ่านสีพื้นผิว
  • หลัก . ฟังก์ชันที่ชื่อว่า main คือโค้ดที่ GPU เรียกใช้งานสำหรับแต่ละพิกเซล และอย่างน้อยต้องสร้างค่าสำหรับ gl_FragColor (เช่น สีของพิกเซลปัจจุบัน)

พิกเซลเชดเดอร์นี้ค่อนข้างเรียบง่าย โดยจะอ่านสีจากพื้นผิวโดยใช้พิกัดพื้นผิวจากจุดสุดยอดเชดเดอร์ (ซึ่งในทางกลับกัน ได้มาจากจุดยอด)

ปัญหาคือเมื่อมีการพัฒนา shaders คุณอยู่เพียงครึ่งทางเพราะคุณต้องจัดการกับโค้ด WebGL จำนวนมาก แท้จริงแล้ว WebGL นั้นทรงพลังมาก แต่ก็อยู่ในระดับต่ำเช่นกัน และคุณต้องทำทุกอย่างด้วยตัวเอง ตั้งแต่การสร้างบัฟเฟอร์ไปจนถึงการกำหนดโครงสร้างจุดยอด คุณต้องคำนวณทั้งหมด ตั้งค่าสถานะทั้งหมด จัดการการโหลดพื้นผิว และอื่นๆ

ยากเกินไป? BABYLON.ShaderMaterial To The Rescue

ฉันรู้ว่าคุณคิดอย่างไร: “Shaders นั้นเจ๋งมาก แต่ฉันไม่อยากยุ่งกับระบบประปาภายในของ WebGL หรือแม้แต่คณิตศาสตร์”

และคุณพูดถูก! นี่เป็นคำถามที่ถูกต้องตามกฎหมาย และนั่นคือเหตุผลที่ฉันสร้าง Babylon.js!

ในการใช้ Babylon.js คุณต้องมีหน้าเว็บที่เรียบง่ายก่อน:

 <!DOCTYPE html> <html> <head> <title>Babylon.js</title> <script src="Babylon.js"></script> <script type="application/vertexShader"> precision highp float; // Attributes attribute vec3 position; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Normal varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; } </script> <script type="application/fragmentShader"> precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); } </script> <script src="index.js"></script> <style> html, body { width: 100%; height: 100%; padding: 0; margin: 0; overflow: hidden; margin: 0px; overflow: hidden; } #renderCanvas { width: 100%; height: 100%; touch-action: none; -ms-touch-action: none; } </style> </head> <body> <canvas></canvas> </body> </html>

คุณจะสังเกตเห็นว่า shaders ถูกกำหนดโดยแท็ก <script> ด้วย .fx คุณยังสามารถกำหนดพวกมันในไฟล์แยกกัน (ไฟล์ .fx)

  • ที่มา Babylon.js
  • ที่เก็บ GitHub

สุดท้าย รหัส JavaScript หลักคือ:

 "use strict"; document.addEventListener("DOMContentLoaded", startGame, false); function startGame() { if (BABYLON.Engine.isSupported()) { var canvas = document.getElementById("renderCanvas"); var engine = new BABYLON.Engine(canvas, false); var scene = new BABYLON.Scene(engine); var camera = new BABYLON.ArcRotateCamera("Camera", 0, Math.PI / 2, 10, BABYLON.Vector3.Zero(), scene); camera.attachControl(canvas); // Creating sphere var sphere = BABYLON.Mesh.CreateSphere("Sphere", 16, 5, scene); var amigaMaterial = new BABYLON.ShaderMaterial("amiga", scene, { vertexElement: "vertexShaderCode", fragmentElement: "fragmentShaderCode", }, { attributes: ["position", "uv"], uniforms: ["worldViewProjection"] }); amigaMaterial.setTexture("textureSampler", new BABYLON.Texture("amiga.jpg", scene)); sphere.material = amigaMaterial; engine.runRenderLoop(function () { sphere.rotation.y += 0.05; scene.render(); }); } };

คุณจะเห็นว่าฉันใช้ BABYLON.ShaderMaterial เพื่อขจัดภาระในการรวบรวม เชื่อมโยง และจัดการกับเฉดสี

เมื่อคุณสร้าง BABYLON.ShaderMaterial คุณต้องระบุองค์ประกอบ DOM ที่ใช้จัดเก็บ shader หรือชื่อฐานของไฟล์ที่มี shader หากคุณเลือกใช้ไฟล์ คุณต้องสร้างไฟล์สำหรับแต่ละ shader และใช้รูปแบบต่อไปนี้: basename.vertex.fx และ basename.fragment.fx จากนั้นคุณจะต้องสร้างสื่อดังนี้:

 var cloudMaterial = new BABYLON.ShaderMaterial("cloud", scene, "./myShader", { attributes: ["position", "uv"], uniforms: ["worldViewProjection"] });

คุณต้องระบุชื่อแอตทริบิวต์และชุดเครื่องแบบที่คุณใช้ด้วย

จากนั้น คุณสามารถตั้งค่าเครื่องแบบและชุดตัวอย่างของคุณได้โดยตรงโดยใช้ setTexture , setFloat , setFloats , setColor3 , setColor4 , setVector2 , setVector3 , setVector4 , setMatrix

ค่อนข้างง่ายใช่มั้ย?

และคุณจำเมทริกซ์ worldViewProjection ก่อนหน้าโดยใช้ Babylon.js และ BABYLON.ShaderMaterial คุณไม่ต้องกังวลกับมัน! BABYLON.ShaderMaterial จะคำนวณให้คุณโดยอัตโนมัติ เนื่องจากคุณจะต้องประกาศในรายการเครื่องแบบ

BABYLON.ShaderMaterial ยังสามารถจัดการเมทริกซ์ต่อไปนี้สำหรับคุณ:

  • world ,
  • view ,
  • projection ,
  • worldView ร์ลวิว ,
  • worldViewProjection กต์

ไม่ต้องใช้คณิตศาสตร์อีกต่อไป ตัวอย่างเช่น ทุกครั้งที่คุณดำเนินการ sphere.rotation.y += 0.05 เมทริกซ์ world ของทรงกลมจะถูกสร้างขึ้นสำหรับคุณและส่งไปยัง GPU

เห็นผลจริงด้วยตัวคุณเอง

สร้าง Shader ของคุณเอง (CYOS)

ตอนนี้ มาขยายความกันและสร้างหน้าที่คุณสามารถสร้างเฉดสีของคุณเองแบบไดนามิกและเห็นผลทันที หน้านี้จะใช้โค้ดเดียวกับที่เราพูดถึงก่อนหน้านี้ และจะใช้อ็อบเจกต์ BABYLON.ShaderMaterial เพื่อคอมไพล์และรัน shader ที่คุณจะสร้าง

ฉันใช้เครื่องมือแก้ไขโค้ด ACE สำหรับ Create Your Own Shader (CYOS) เป็นโปรแกรมแก้ไขโค้ดที่น่าทึ่งพร้อมการเน้นไวยากรณ์ รู้สึกอิสระที่จะมองไปที่มัน

เมื่อใช้คอมโบบ็อกซ์แรก คุณจะสามารถเลือกเฉดสีที่กำหนดไว้ล่วงหน้าได้ เราจะเห็นแต่ละคนทันทีหลังจากนั้น

คุณยังสามารถเปลี่ยนเมช (เช่น ออบเจ็กต์ 3 มิติ) ที่ใช้เพื่อดูตัวอย่างเฉดสีของคุณโดยใช้กล่องคำสั่งผสมที่สอง

ปุ่มคอมไพล์ใช้เพื่อสร้าง BABYLON.ShaderMaterial ใหม่จากเฉดสีของคุณ รหัสที่ใช้โดยปุ่มนี้มีดังนี้:

 // Compile shaderMaterial = new BABYLON.ShaderMaterial("shader", scene, { vertexElement: "vertexShaderCode", fragmentElement: "fragmentShaderCode", }, { attributes: ["position", "normal", "uv"], uniforms: ["world", "worldView", "worldViewProjection"] }); var refTexture = new BABYLON.Texture("ref.jpg", scene); refTexture.wrapU = BABYLON.Texture.CLAMP_ADDRESSMODE; refTexture.wrapV = BABYLON.Texture.CLAMP_ADDRESSMODE; var amigaTexture = new BABYLON.Texture("amiga.jpg", scene); shaderMaterial.setTexture("textureSampler", amigaTexture); shaderMaterial.setTexture("refSampler", refTexture); shaderMaterial.setFloat("time", 0); shaderMaterial.setVector3("cameraPosition", BABYLON.Vector3.Zero()); shaderMaterial.backFaceCulling = false; mesh.material = shaderMaterial;

ง่ายอย่างไม่น่าเชื่อใช่มั้ย? สื่อพร้อมที่จะส่งเมทริกซ์ที่คำนวณล่วงหน้าสามชุด ( world , worldView และ worldViewProjection ) จุดยอดจะมาพร้อมกับตำแหน่งพิกัดปกติและพื้นผิว มีการโหลดพื้นผิวสองแบบสำหรับคุณแล้ว:

amiga.jpg (ดูเวอร์ชันขนาดใหญ่)
ref.jpg (ดูเวอร์ชันขนาดใหญ่)

ในที่สุด renderLoop เป็นที่ที่ฉันอัปเดตสองชุดที่สะดวก:

  • หนึ่งเรียกว่า time และได้รับแอนิเมชั่นตลก ๆ
  • อีกอันเรียกว่า cameraPosition ซึ่งจะกำหนดตำแหน่งของกล้องในเฉดสีของคุณ (มีประโยชน์สำหรับสมการแสง)

 engine.runRenderLoop(function () { mesh.rotation.y += 0.001; if (shaderMaterial) { shaderMaterial.setFloat("time", time); time += 0.02; shaderMaterial.setVector3("cameraPosition", camera.position); } scene.render(); });

เชดเดอร์พื้นฐาน

มาเริ่มกันที่ shader แรกที่กำหนดไว้ใน CYOS: shader พื้นฐาน

เรารู้จักเชดเดอร์นี้แล้ว มันคำนวณ gl_position และใช้พิกัดพื้นผิวเพื่อดึงสีสำหรับทุกพิกเซล

ในการคำนวณตำแหน่งพิกเซล เราต้องใช้เมทริกซ์ worldViewProjection และตำแหน่งของจุดยอด:

 precision highp float; // Attributes attribute vec3 position; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; }

พิกัดพื้นผิว ( uv ) จะถูกส่งแบบไม่แก้ไขไปยังตัวแบ่งพิกเซล

โปรดทราบว่าเราจำเป็นต้องเพิ่ม precision mediump float ที่มีความแม่นยำในบรรทัดแรกสำหรับทั้งจุดยอดและตัวแบ่งพิกเซลเนื่องจาก Chrome ต้องการ ระบุว่าเพื่อประสิทธิภาพที่ดีขึ้น เราไม่ใช้ค่าลอยตัวที่มีความแม่นยำเต็มรูปแบบ

pixel shader นั้นง่ายยิ่งขึ้นไปอีก เพราะเราแค่ต้องใช้พิกัดพื้นผิวและดึงสีพื้นผิว:

 precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); }

ก่อนหน้านี้เราเห็นว่าชุด textureSampler นั้นเต็มไปด้วยพื้นผิวของ amiga ดังนั้นผลลัพธ์จึงเป็นดังนี้:

(ดูรุ่นใหญ่)

เชดเดอร์ขาวดำ

มาต่อกันด้วย shader ใหม่: shader ขาวดำ เป้าหมายของ shader นี้คือการใช้อันก่อนหน้า แต่มีโหมดการเรนเดอร์แบบขาวดำเท่านั้น

ในการทำเช่นนั้น เราสามารถเก็บเชดเดอร์จุดยอดเดียวกันได้ พิกเซลเชดเดอร์จะได้รับการแก้ไขเล็กน้อย

ตัวเลือกแรกที่เรามีคือใช้องค์ประกอบเดียวเท่านั้น เช่น องค์ประกอบสีเขียว:

 precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = vec4(texture2D(textureSampler, vUV).ggg, 1.0); }

อย่างที่คุณเห็น แทนที่จะใช้ .rgb (การดำเนินการนี้เรียกว่า swizzle) เราใช้ . .ggg

แต่ถ้าเราต้องการเอฟเฟกต์ขาวดำที่แม่นยำจริงๆ การคำนวณความส่องสว่าง (ซึ่งคำนึงถึงองค์ประกอบทั้งหมด) จะดีกว่า:

 precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { float luminance = dot(texture2D(textureSampler, vUV).rgb, vec3(0.3, 0.59, 0.11)); gl_FragColor = vec4(luminance, luminance, luminance, 1.0); }

การดำเนินการ dot (หรือผล dot ) คำนวณดังนี้: result = v0.x * v1.x + v0.y * v1.y + v0.z * v1.z

ในกรณีของเรา luminance = r * 0.3 + g * 0.59 + b * 0.11 . (ค่าเหล่านี้อิงจากการที่ดวงตาของมนุษย์ไวต่อแสงสีเขียวมากกว่า)

ฟังดูดีใช่มั้ย?

(ดูรุ่นใหญ่)

เซลล์แรเงา Shader

มาเริ่มกันที่ shader ที่ซับซ้อนกว่ากัน: shader เซลล์

สิ่งนี้จะทำให้เราต้องทำให้จุดยอดปกติและตำแหน่งของจุดยอดเป็นพิกเซลเชดเดอร์ ดังนั้น จุดยอดเชเดอร์จะมีลักษณะดังนี้:

 precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 world; uniform mat4 worldViewProjection; // Varying varying vec3 vPositionW; varying vec3 vNormalW; varying vec2 vUV; void main(void) { vec4 outPosition = worldViewProjection * vec4(position, 1.0); gl_Position = outPosition; vPositionW = vec3(world * vec4(position, 1.0)); vNormalW = normalize(vec3(world * vec4(normal, 0.0))); vUV = uv; }

โปรดทราบว่าเรายังใช้เมทริกซ์โลกด้วยเนื่องจากตำแหน่งและค่าปกติถูกเก็บไว้โดยไม่มีการเปลี่ยนแปลงใดๆ และเราต้องใช้เมทริกซ์โลกเพื่อพิจารณาการหมุนของวัตถุ

ตัวแบ่งพิกเซลเป็นดังนี้:

 precision highp float; // Lights varying vec3 vPositionW; varying vec3 vNormalW; varying vec2 vUV; // Refs uniform sampler2D textureSampler; void main(void) { float ToonThresholds[4]; ToonThresholds[0] = 0.95; ToonThresholds[1] = 0.5; ToonThresholds[2] = 0.2; ToonThresholds[3] = 0.03; float ToonBrightnessLevels[5]; ToonBrightnessLevels[0] = 1.0; ToonBrightnessLevels[1] = 0.8; ToonBrightnessLevels[2] = 0.6; ToonBrightnessLevels[3] = 0.35; ToonBrightnessLevels[4] = 0.2; vec3 vLightPosition = vec3(0, 20, 10); // Light vec3 lightVectorW = normalize(vLightPosition - vPositionW); // diffuse float ndl = max(0., dot(vNormalW, lightVectorW)); vec3 color = texture2D(textureSampler, vUV).rgb; if (ndl > ToonThresholds[0]) { color *= ToonBrightnessLevels[0]; } else if (ndl > ToonThresholds[1]) { color *= ToonBrightnessLevels[1]; } else if (ndl > ToonThresholds[2]) { color *= ToonBrightnessLevels[2]; } else if (ndl > ToonThresholds[3]) { color *= ToonBrightnessLevels[3]; } else { color *= ToonBrightnessLevels[4]; } gl_FragColor = vec4(color, 1.); }

เป้าหมายของ Shader นี้คือการจำลองแสง และแทนที่จะใช้การแรเงาที่ราบรื่น เราจะใช้แสงตามเกณฑ์ความสว่างที่เฉพาะเจาะจง ตัวอย่างเช่น หากความเข้มของแสงอยู่ระหว่าง 1 (สูงสุด) ถึง 0.95 สีของวัตถุ (ดึงมาจากพื้นผิว) จะถูกนำไปใช้โดยตรง หากความเข้มอยู่ระหว่าง 0.95 ถึง 0.5 สีจะถูกลดทอนด้วย 0.8 เท่า และอื่นๆ.

ส่วนใหญ่มีสี่ขั้นตอนใน shader นี้

ขั้นแรก เราประกาศค่าคงที่ขีดจำกัดและระดับ

จากนั้นเราคำนวณแสงโดยใช้สมการพงษ์ (เราจะพิจารณาว่าแสงไม่เคลื่อนที่):

 vec3 vLightPosition = vec3(0, 20, 10); // Light vec3 lightVectorW = normalize(vLightPosition - vPositionW); // diffuse float ndl = max(0., dot(vNormalW, lightVectorW));

ความเข้มของแสงต่อพิกเซลขึ้นอยู่กับมุมระหว่างทิศทางปกติและทิศทางของแสง

จากนั้น เราจะได้สีพื้นผิวสำหรับพิกเซล

สุดท้าย เราตรวจสอบเกณฑ์และใช้ระดับกับสี

ผลลัพธ์ดูเหมือนวัตถุการ์ตูน:

(ดูรุ่นใหญ่)

พงษ์ เชเดอร์

เราใช้ส่วนหนึ่งของสมการพงษ์ในเชดเดอร์ก่อนหน้า มาใช้กันให้หมดตอนนี้เลย

จุดสุดยอดเชดเดอร์นั้นเรียบง่ายอย่างชัดเจนที่นี่ เพราะทุกอย่างจะทำในพิกเซลเชดเดอร์:

 precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUV; void main(void) { vec4 outPosition = worldViewProjection * vec4(position, 1.0); gl_Position = outPosition; vUV = uv; vPosition = position; vNormal = normal; }

ตามสมการ เราต้องคำนวณส่วน "กระจาย" และ "สเปกตรัม" โดยใช้ทิศทางแสงและจุดยอดปกติ:

 precision highp float; // Varying varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUV; // Uniforms uniform mat4 world; // Refs uniform vec3 cameraPosition; uniform sampler2D textureSampler; void main(void) { vec3 vLightPosition = vec3(0, 20, 10); // World values vec3 vPositionW = vec3(world * vec4(vPosition, 1.0)); vec3 vNormalW = normalize(vec3(world * vec4(vNormal, 0.0))); vec3 viewDirectionW = normalize(cameraPosition - vPositionW); // Light vec3 lightVectorW = normalize(vLightPosition - vPositionW); vec3 color = texture2D(textureSampler, vUV).rgb; // diffuse float ndl = max(0., dot(vNormalW, lightVectorW)); // Specular vec3 angleW = normalize(viewDirectionW + lightVectorW); float specComp = max(0., dot(vNormalW, angleW)); specComp = pow(specComp, max(1., 64.)) * 2.; gl_FragColor = vec4(color * ndl + vec3(specComp), 1.); }

เราใช้ส่วนกระจายในเชดเดอร์ก่อนหน้าแล้ว ดังนั้นที่นี่เราเพียงแค่ต้องเพิ่มส่วนพิเศษ คุณสามารถหาข้อมูลเพิ่มเติมเกี่ยวกับการแรเงาพงษ์ได้ในวิกิพีเดีย

ผลลัพธ์ของทรงกลมของเรา:

(ดูรุ่นใหญ่)

ทิ้ง Shader

สำหรับ discard shader ฉันอยากจะแนะนำแนวคิดใหม่: คำหลัก discard

Shader นี้ทิ้งทุกพิกเซลที่ไม่ใช่สีแดงและสร้างภาพลวงตาของวัตถุที่ขุด

จุดสุดยอด Shader เหมือนกับที่ใช้โดย shader พื้นฐาน:

 precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; }

พิกเซลเชเดอร์ที่ด้านข้างจะต้องทดสอบสีและใช้ทิ้งเมื่อองค์ประกอบสีเขียวสูงเกินไป ตัวอย่างเช่น

 precision highp float; varying vec2 vUV; // Refs uniform sampler2D textureSampler; void main(void) { vec3 color = texture2D(textureSampler, vUV).rgb; if (color.g > 0.5) { discard; } gl_FragColor = vec4(color, 1.); }

ผลที่ได้คือเรื่องตลกเล็กน้อย:

(ดูรุ่นใหญ่)

Wave Shader

เราเล่น Pixel Shader มามากแล้ว แต่ฉันต้องการแจ้งให้คุณทราบด้วยว่าเราสามารถทำสิ่งต่างๆ ได้มากมายด้วยจุดยอด Shader

สำหรับ wave shader เราจะใช้ Phong pixel shader ซ้ำ

เวอร์เท็กซ์เชเดอร์จะใช้ชุดชื่อ time เพื่อรับค่าภาพเคลื่อนไหว การใช้เครื่องแบบนี้ shader จะสร้างคลื่นที่มีตำแหน่งของจุดยอด:

 precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; uniform float time; // Varying varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUV; void main(void) { vec3 v = position; vx += sin(2.0 * position.y + (time)) * 0.5; gl_Position = worldViewProjection * vec4(v, 1.0); vPosition = position; vNormal = normal; vUV = uv; }

ไซนัสถูกนำไปใช้กับ position.y และผลลัพธ์จะเป็นดังนี้:

(ดูรุ่นใหญ่)

การทำแผนที่สภาพแวดล้อมทรงกลม

บทความนี้ได้รับ แรง บันดาลใจจากบทความเรื่อง "Creating a Spherical Reflection/Environment Mapping Shader" ฉันจะให้คุณอ่านบทความที่ยอดเยี่ยมและเล่นกับ shader ที่เกี่ยวข้อง

(ดูรุ่นใหญ่)

Fresnel Shader

ฉันต้องการสรุปบทความนี้ด้วยสิ่งที่ฉันโปรดปราน: Fresnel shader

เฉดสีนี้ใช้เพื่อให้ความเข้มต่างกันไปตามมุมระหว่างทิศทางการมองและจุดยอดปกติ

เวอร์เท็กซ์เชดเดอร์เป็นแบบเดียวกับที่ใช้โดยแชเดอร์แรเงาเซลล์ และเราสามารถคำนวณคำศัพท์เฟรสในพิกเซลเชดเดอร์ของเรา (เพราะเรามีตำแหน่งปกติและตำแหน่งของกล้อง ซึ่งสามารถใช้ในการประเมินทิศทางการมองได้):

 precision highp float; // Lights varying vec3 vPositionW; varying vec3 vNormalW; // Refs uniform vec3 cameraPosition; uniform sampler2D textureSampler; void main(void) { vec3 color = vec3(1., 1., 1.); vec3 viewDirectionW = normalize(cameraPosition - vPositionW); // Fresnel float fresnelTerm = dot(viewDirectionW, vNormalW); fresnelTerm = clamp(1.0 - fresnelTerm, 0., 1.); gl_FragColor = vec4(color * fresnelTerm, 1.); } 
(ดูรุ่นใหญ่)

เชดเดอร์ของคุณ?

ตอนนี้คุณพร้อมที่จะสร้าง shader ของคุณเองแล้ว อย่าลังเลที่จะโพสต์ในฟอรัม Babylon.js เพื่อแบ่งปันการทดลองของคุณ!

หากคุณต้องการไปต่อ ต่อไปนี้คือลิงก์ที่มีประโยชน์:

  • Babylon.js เว็บไซต์อย่างเป็นทางการ
  • Babylon.js, ที่เก็บ GitHub
  • ฟอรั่ม Babylon.js, HTML5 Game Devs
  • สร้าง Shader ของคุณเอง (CYOS), Babylon.js
  • ภาษาแรเงา OpenGL” Wikipedia
  • ภาษาแรเงา OpenGL, เอกสารประกอบ