สร้างเฉดสีด้วย Babylon.js
เผยแพร่แล้ว: 2022-03-10Shaders เป็นแนวคิดหลักหากคุณต้องการปลดปล่อยพลังดิบของ 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
) จุดยอดจะมาพร้อมกับตำแหน่งพิกัดปกติและพื้นผิว มีการโหลดพื้นผิวสองแบบสำหรับคุณแล้ว:
ในที่สุด 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, เอกสารประกอบ