
هناك عدد قليل جدًا من مقدمات أساسيات العمل مع Three.js عبر الإنترنت ، لكن قد تلاحظ نقصًا في المواد المتعلقة بالمواضيع الأكثر تقدمًا. وأحد هذه المواضيع هو مزيج من التظليل والمشاهد مع النماذج ثلاثية الأبعاد. في نظر العديد من المطورين المبتدئين ، هو مثل أشياء غير متوافقة من عوالم مختلفة. اليوم ، باستخدام مثال بسيط لـ "كرة البلازما" ، سنرى ما هو ShaderMaterial وماهية أكله ، وما تأثير Effect ، ومدى السرعة التي يمكن القيام بها بعد المعالجة للمشهد المقدم.
من المفترض أن يكون القارئ معتادًا على أساسيات العمل مع Three.js ويفهم كيفية عمل التظليل. إذا لم تواجه هذا من قبل ، فأنا أوصي بشدة بقراءة هذا أولاً:
ولكن لنبدأ ...
ShaderMaterial - ما هو؟
لقد رأينا بالفعل كيف يتم استخدام نسيج مسطح وكيف يتم تمديده على كائن ثلاثي الأبعاد. كما كان هذا الملمس صورة عادية. عندما درسنا كتابة شظايا الشظايا ، كان كل شيء مسطحًا هناك أيضًا. لذا: إذا استطعنا استخدام تظليل لإنشاء صورة مسطحة ، فلماذا لا نستخدمها كملمس؟
هذه هي الفكرة التي تشكل الأساس لتظليل المواد. عند إنشاء مادة لكائن ثلاثي الأبعاد ، فإننا نشير إلى تظليل بدلاً من نسيج له. في شكله الأساسي ، يبدو مثل هذا:
const shaderMaterial = new THREE.ShaderMaterial({ uniforms: {
سيتم استخدام أداة تظليل الأجزاء لإنشاء نسيج المادة ، وأنت تسأل بالطبع ، ماذا ستفعل تظليل قمة الرأس؟ هل سيقوم مرة أخرى بإعادة فرز الأصوات بشكل طبيعي؟ نعم ، سنبدأ بهذا الخيار البسيط ، لكن يمكننا أيضًا تعيين إزاحة أو إجراء عمليات تلاعب أخرى لكل رأس من كائن ثلاثي الأبعاد - الآن لا توجد قيود على الطائرة. لكن من الأفضل رؤية كل هذا بمثال. وبكلمات ، فهم القليل. اصنع مشهدًا واجعل كرة واحدة في الوسط.

كمواد للكرة ، سوف نستخدم ShaderMaterial:
const geometry = new THREE.SphereBufferGeometry(30, 64, 64); const shaderMaterial = new THREE.ShaderMaterial({ uniforms: {
سوف تظليل قمة الرأس تكون محايدة:
void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
لاحظ أن Three.js بتمرير متغيرات موحدة. ليس علينا أن نفعل أي شيء ، فهي ضمنية. في حد ذاتها ، فإنها تحتوي على جميع أنواع المصفوفات ، والتي لدينا بالفعل الوصول إليها من JS ، وكذلك موضع الكاميرا. تخيل أنه في بداية التظليل أنفسهم يتم إدراج شيء:
بالإضافة إلى ذلك ، يتم تمرير العديد من متغيرات السمات إلى تظليل قمة الرأس:
attribute vec3 position; attribute vec3 normal; attribute vec2 uv;
بالأسماء ، من الواضح ما هي - موقع قمة الرأس الحالية ، والموضع الطبيعي على السطح في هذه المرحلة ، والإحداثيات على النسيج الذي تتوافق مع قمة الرأس.
تقليديا ، يتم تعيين الإحداثيات في الفضاء على أنها (س ، ص ، ض) ، والإحداثيات على الطائرة الملمس بأنها (ش ، ت). ومن هنا جاء اسم المتغير. سوف تقابله في كثير من الأحيان في أمثلة مختلفة. من الناحية النظرية ، نحتاج إلى نقل هذه الإحداثيات إلى تظليل الأجزاء من أجل العمل معهم هناك. سنفعل ذلك.
varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
لبداية ، يجب أن يكون تظليل الأجزاء مثل هذا:
#define EPSILON 0.02 varying vec2 vUv; void main() { if ((fract(vUv.x * 10.0) < EPSILON) || (fract(vUv.y * 10.0) < EPSILON)) { gl_FragColor = vec4(vec3(0.0), 1.0); } else { gl_FragColor = vec4(1.0); } }
مجرد إنشاء شبكة. إذا كنت تفكر قليلاً ، فعندئذٍ ستكون الطائرة مجرد شبكة من المربعات ، ولكن بما أننا نركبها على كرة ، فهي مشوهة وتتحول إلى كرة أرضية. هناك صورة جيدة على ويكيبيديا توضح ما يحدث:

هذا هو ، في تظليل الشظية ، نصنع نسيجًا مسطحًا ، كما في الوسط في هذا الرسم التوضيحي ، ثم تسحبه Three.js على الكرة. مريح جدا
بالطبع ، بالنسبة للنماذج الأكثر تعقيدًا ، ستكون عملية المسح أكثر تعقيدًا. لكن عادة عند إنشاء مواقع تصميم متنوعة ، فإننا نعمل بأشكال هندسية بسيطة ومن السهل تخيل اكتساح في رأسك.
حسنا ، ماذا يمكنك أن تفعل حيال ذلك؟
الميزة الرئيسية هي أن مواد التظليل يمكن أن تتغير بمرور الوقت. هذا ليس بالأمر الثابت الذي رسمناه مرة واحدة ونسيناه ، يمكننا تحريكه. علاوة على ذلك ، سواء في اللون (في شظية شظية) وفي الشكل (في قمة الرأس). هذه هي أداة قوية جدا.
في مثالنا ، سنجعل النار تطوق كرة. سيكون هناك مجالان - واحد عادي (من الداخل) ، والثاني من مادة التظليل (من الخارج ، بنصف قطر كبير). إضافة مجال آخر لن يعلق.

أولاً ، أضف الوقت كمتغير منتظم للتظليل في مادتنا. لا مكان دون وقت. لقد فعلنا هذا بالفعل في JS خالص ، لكن في Three.js الأمر بهذه البساطة. دع الوقت في التظليل يسمى uTime ، ويتم تخزينه في متغير TIME:
function updateUniforms() { SCENE.traverse((child) => { if (child instanceof THREE.Mesh && child.material.type === 'ShaderMaterial') { child.material.uniforms.uTime.value = TIME; child.material.needsUpdate = true; } }); }
نقوم بتحديث كل شيء مع كل مكالمة إلى وظيفة تنشيط:
function animate() { requestAnimationFrame(animate); TIME += 0.005; updateUniforms(); render(); }
النار
إنشاء النار يشبه إلى حد كبير إنشاء منظر طبيعي ، ولكن بدلاً من المرتفعات ، اللون. أو الشفافية ، كما في حالتنا.
وظائف للعشوائية والضوضاء التي رأيناها بالفعل ، لن نقوم بتحليلها بالتفصيل. كل ما نحتاج إلى القيام به هو جعل الضوضاء على ترددات مختلفة لإضافة مجموعة متنوعة ، وجعل كل من هذه الضوضاء تتحرك بسرعات مختلفة. ستحصل على شيء مثل اللهب ، الكبيرة تتحرك ببطء ، الصغيرة تتحرك أسرع:
uniform float uTime; varying vec2 vUv; float rand(vec2); float noise(vec2); void main() { vec2 position1 = vec2(vUv.x * 4.0, vUv.y - uTime); vec2 position2 = vec2(vUv.x * 4.0, vUv.y - uTime * 2.0); vec2 position3 = vec2(vUv.x * 4.0, vUv.y - uTime * 3.0); float color = ( noise(position1 * 5.0) + noise(position2 * 10.0) + noise(position3 * 15.0)) / 3.0; gl_FragColor = vec4(0.0, 0.0, 0.0, color - smoothstep(0.1, 1.3, vUv.y)); } float rand(vec2 seed) { return fract(sin(dot(seed, vec2(12.9898,78.233))) * 43758.5453123); } float noise(vec2 position) { vec2 blockPosition = floor(position); float topLeftValue = rand(blockPosition); float topRightValue = rand(blockPosition + vec2(1.0, 0.0)); float bottomLeftValue = rand(blockPosition + vec2(0.0, 1.0)); float bottomRightValue = rand(blockPosition + vec2(1.0, 1.0)); vec2 computedValue = smoothstep(0.0, 1.0, fract(position)); return mix(topLeftValue, topRightValue, computedValue.x) + (bottomLeftValue - topLeftValue) * computedValue.y * (1.0 - computedValue.x) + (bottomRightValue - topRightValue) * computedValue.x * computedValue.y; }
حتى لا يغطي اللهب الكرة بأكملها ، نلعب مع معلمة اللون الرابعة - الشفافية - ونربطها بالتنسيق y. في حالتنا ، هذا الخيار مناسب للغاية. بعبارات أعم ، نطبق تدرجًا شفافًا على الضوضاء.
في مثل هذه الأوقات ، من المفيد تذكر وظيفة السلس.
بشكل عام ، مثل هذا النهج لخلق النار باستخدام تظليل هو كلاسيكي. سوف تقابله في كثير من الأحيان في أماكن مختلفة. سيكون من المفيد اللعب بالأرقام السحرية - يتم تعيينها بشكل عشوائي في المثال ، وكيف سيبدو البلازما يعتمد عليها.
لجعل النار أكثر إثارة للاهتمام ، دعنا ننتقل إلى تظليل قمة الرأس وشامان قليلا ...
كيفية جعل الشعلة قليلا "سكب" في الفضاء؟ بالنسبة للمبتدئين ، يمكن أن يسبب هذا السؤال صعوبات كبيرة ، على الرغم من بساطته. لقد رأيت طرقًا معقدة للغاية لحل هذه المشكلة ، لكن في جوهرها - نحتاج إلى تحريك الرؤوس بسلاسة على الكرة وفقًا للخطوط "من مركزها". إلى جيئة وذهابا ، جيئة وذهابا. لقد مرت علينا Three.js بالفعل الموضع الحالي من قمة الرأس وطبيعية - سنستخدمها. بالنسبة لـ "للخلف وللأمام" ، ستناسب بعض الوظائف المحدودة ، على سبيل المثال الجيب. يمكنك بالطبع تجربة ، ولكن الجيب هو الخيار الافتراضي.
لا أعرف ماذا تأخذ - خذ الجيب. الأفضل من ذلك ، مجموع الجيب مع ترددات مختلفة.
نحول الإحداثيات بشكل طبيعي إلى القيمة التي تم الحصول عليها ونعيد حسابها وفقًا للصيغة المعروفة سابقًا.
uniform float uTime; varying vec2 vUv; void main() { vUv = uv; vec3 delta = normal * sin(position.x * position.y * uTime / 10.0); vec3 newPosition = position + delta; gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0); }
ما حصلنا عليه لم يعد مجالًا كبيرًا. هذا ... أنا لا أعرف حتى إذا كان هذا واحد له اسم. لكن ، مرة أخرى ، لا تنس أن تتغلب على الصعاب - فهي تؤثر كثيرًا. عند إنشاء مثل هذه التأثيرات ، غالبًا ما يتم اختيار شيء ما عن طريق التجربة والخطأ ومن المفيد جدًا تطوير "الحدس الرياضي" في نفسه - القدرة على تخيل أكثر أو أقل كيف تتصرف الوظيفة ، وكيف تعتمد على المتغيرات.
في هذه المرحلة ، لدينا صورة مثيرة للاهتمام ، لكن خرقاء بعض الشيء. أولاً ، دعونا نلقي نظرة على مرحلة ما بعد المعالجة ، ثم ننتقل إلى مثال حي.
بعد المعالجة
تعد القدرة على القيام بشيء ما باستخدام صورة Three.js المقدمة شيئًا مفيدًا للغاية ، بينما يتم نسيانها بشكل غير مستحق في العديد من سلسلة الدروس. من الناحية الفنية ، يتم تنفيذ ذلك على النحو التالي: يتم إرسال الصورة التي قدمها لنا العارض إلى EffectComposer (طالما كان الصندوق أسودًا) ، مما يؤدي إلى إضفاء الحيوية على شيء في حد ذاته ويعرض الصورة النهائية على قماش. وهذا هو ، بعد العارض ، يتم إضافة وحدة واحدة أخرى. ننقل المعلمات إلى هذا الملحن - ما يجب القيام به مع الصورة المستلمة. تسمى هذه المعلمة pass. بمعنى ما ، يعمل الملحن مثل بعض Gulp - إنه لا يفعل شيئًا ، فنحن نمنحه الإضافات التي تقوم بالفعل بالعمل. ربما ليس صحيحًا تمامًا قول ذلك ، لكن يجب أن تكون الفكرة واضحة.
لا يتم تضمين كل ما سوف نستخدمه بشكل أكبر في الهيكل الأساسي لثلاثيات ، لذلك نربط بعض التبعيات والتبعيات الخاصة بالتبعيات نفسها:
<script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/EffectComposer.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/RenderPass.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/ShaderPass.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/shaders/CopyShader.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/shaders/LuminosityHighPassShader.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/postprocessing/UnrealBloomPass.js'></script>
تذكر أن هذه البرامج النصية مضمنة في الحزمة الثلاثة ويمكنك وضع كل ذلك في حزمة واحدة باستخدام حزمة الويب أو نظائرها.
في شكله الأساسي ، يتم إنشاء الملحن مثل هذا:
COMPOSER = new THREE.EffectComposer(RENDERER); COMPOSER.setSize(window.innerWidth, window.innerHeight); const renderPass = new THREE.RenderPass(SCENE, CAMERA); renderPass.renderToScreen = true; COMPOSER.addPass(renderPass);
RenderPass لا تفعل في الواقع أي شيء جديد. إنه يعرض ما اعتدنا الحصول عليه من عارض منتظم. في الواقع ، إذا نظرت إلى شفرة المصدر لـ RenderPass ، يمكنك العثور على العارض القياسي هناك. منذ الآن ، يحدث العرض هناك ، نحتاج إلى استبدال العارض بالملحن في برنامجنا النصي:
function render() {
هذا النهج باستخدام RenderPass كأول تمرير هو ممارسة قياسية عند العمل مع EffectComposer. عادة ما نحتاج أولاً إلى الحصول على صورة مقدمة للمشهد ، ثم القيام بشيء ما.
في الأمثلة من Three.js ، في قسم postprocessing ، يمكنك العثور على شيء يسمى UnrealBloomPass. هذا هو البرنامج النصي المنقول من محرك غير واقعي. يضيف توهجًا صغيرًا يمكن استخدامه لإنشاء إضاءة أكثر جمالا. في كثير من الأحيان ستكون هذه هي الخطوة الأولى لتحسين الصورة.
const bloomPass = new THREE.UnrealBloomPass( new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 1, 0.1); bloomPass.renderToScreen = true; COMPOSER.addPass(bloomPass);
يرجى ملاحظة: يتم تعيين خيار renderToScreen فقط على آخر تصريح تم تمريره إلى الملحن.
ولكن دعونا نرى بالفعل نوع التوهج الذي أعطانا هذا bloomPass وكيف يتناسب مع الكرة:
توافق ، هذا أكثر إثارة للاهتمام من مجرد مجال ومصدر ضوء عادي ، كما تظهر عادةً في الدروس الأولية على Three.js.
ولكن سنذهب أبعد من ذلك ...
المزيد من التظليل إلى الإله شادر!

من المفيد جدًا استخدام console.log وإلقاء نظرة على بنية الملحن. في ذلك ، يمكنك العثور على بعض العناصر ذات الأسماء renderTarget1 ، renderTarget2 ، وما إلى ذلك ، حيث تتوافق الأرقام مع الأرقام القياسية للمرور. ثم يصبح من الواضح لماذا يسمى EffectComposer. إنه يعمل على مبدأ المرشحات في SVG. تذكر ، هناك يمكنك استخدام نتيجة أداء بعض المرشحات في غيرها؟ هنا نفس الشيء - يمكنك الجمع بين الآثار.
يعد استخدام console.log لفهم البنية الداخلية لكائنات Three.js والعديد من المكتبات الأخرى مفيدًا للغاية. استخدم هذا النهج في كثير من الأحيان لفهم ما هو أفضل.
إضافة تمريرة أخرى. هذه المرة سيكون ShaderPass.
const shader = { uniforms: { uRender: { value: COMPOSER.renderTarget2 }, uTime: { value: TIME } }, vertexShader: document.getElementById('postprocessing-vertex-shader').textContent, fragmentShader: document.getElementById('postprocessing-fragment-shader').textContent }; const shaderPass = new THREE.ShaderPass(shader); shaderPass.renderToScreen = true; COMPOSER.addPass(shaderPass);
يحتوي RenderTarget2 على نتيجة التمرير السابق - bloomPass (كانت الثانية في صف واحد) ، ونحن نستخدمها كملمس (هذه صورة مسطحة بشكل أساسي) ونمررها كمتغير منتظم إلى التظليل الجديد.
ربما يستحق الكبح وتحقيق كل السحر هنا ...
بعد ذلك ، قم بإنشاء تظليل قمة بسيط. في معظم الحالات ، في هذه المرحلة ، لا نحتاج إلى عمل أي شيء مع القمم ، فنحن نمر فقط بالإحداثيات (u ، v) إلى تظليل الأجزاء:
varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
وفي مجزأة يمكننا أن نستمتع بطعمنا ولوننا. على سبيل المثال ، يمكننا إضافة تأثير خلل الضوء ، وجعل كل شيء بالأبيض والأسود واللعب مع السطوع / التباين:
uniform sampler2D uRender; uniform float uTime; varying vec2 vUv; float rand(vec2); void main() { float randomValue = rand(vec2(floor(vUv.y * 7.0), uTime / 1.0)); vec4 color; if (randomValue < 0.02) { color = texture2D(uRender, vec2(vUv.x + randomValue - 0.01, vUv.y)); } else { color = texture2D(uRender, vUv); } float lightness = (color.r + color.g + color.b) / 3.0; color.rgb = vec3(smoothstep(0.02, 0.7, lightness)); gl_FragColor = color; } float rand(vec2 seed) { return fract(sin(dot(seed, vec2(12.9898,78.233))) * 43758.5453123); }
لنلقِ نظرة على النتيجة:
كما ترون ، تم تثبيت المرشحات على الكرة. لا يزال ثلاثي الأبعاد ، لم ينكسر أي شيء ، لكن على اللوحة لدينا صورة مُعالجة.
الخاتمة
تعتبر مواد التظليل والمعالجة اللاحقة في Three.js من الأدوات الصغيرة ، لكنها قوية جدًا والتي تستحق بالتأكيد استخدامها. هناك الكثير من الخيارات لاستخدامها - كل شيء محدود بخيالك. حتى أبسط المشاهد بمساعدتهم يمكن تغييرها إلى ما بعد الاعتراف.