
جميع أنواع عروض المنتجات التقديمية ثلاثية الأبعاد ليست نادرة جدًا في عصرنا ، لكن هذه المهام تسبب الكثير من الأسئلة من المطورين الأوائل. سننظر اليوم في بعض الأساسيات التي ستساعدك على الدخول في هذا الموضوع وعدم التعثر في مهمة بسيطة مثل عرض نموذج ثلاثي الأبعاد في المتصفح. كوسيلة مساعدة ، سوف نستخدم Three.js كأكثر الأدوات شعبية في هذا المجال.
الحصول على العمل
بادئ ذي بدء ، لنصنع قالب HTML لأنفسنا. حتى لا نعقد المثال ، فلن نستخدم أي شيء لا لزوم له ، ولا مجمعين ، أو معالجات سابقة ، إلخ.
نحتاج إلى حاوية للوحات القماش ومجموعة من البرامج النصية - في الواقع three.js ، محمل للنماذج بتنسيق obj ، وبرنامج نصي للتحكم في الكاميرا باستخدام الماوس.
<div class='canvas-container'></div> <script src='https://unpkg.com/three@0.99.0/build/three.min.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/loaders/OBJLoader.js'></script> <script src='https://unpkg.com/three@0.99.0/examples/js/controls/OrbitControls.js'></script> <script src='./main.js'></script>
إذا كان مشروعك يستخدم NPM والبناة ، فيمكنك استيراد كل هذا من الحزمة الثالثة. في الواقع ، إذا كان شخص ما لا يعرف ، يأخذ Unpkg جميع حزم NPM.
إذا كنت بحاجة إلى توصيل شيء ما بسرعة من حزمة ما إلى صفحتك ، لكنك لم تجد الرابط إلى شبكة CDN ، تذكر إلغاء تحديد موقع الإنترنت ، على الأرجح أنك في حاجة إليه.
سيبدأ النص الرئيسي بمجموعة من المتغيرات العالمية. هذا سوف تبسيط المثال.
let SCENE; let CAMERA; let RENDERER; let LOADING_MANAGER; let IMAGE_LOADER; let OBJ_LOADER; let CONTROLS; let MOUSE; let RAYCASTER; let TEXTURE; let OBJECT;
في Three.js ، يبدأ كل شيء بالمشهد ، لذلك قم بتهيئته وإنشاء اثنين من مصادر الضوء:
function initScene() { SCENE = new THREE.Scene(); initLights(); } function initLights() { const ambient = new THREE.AmbientLight(0xffffff, 0.7); SCENE.add(ambient); const directionalLight = new THREE.DirectionalLight(0xffffff); directionalLight.position.set(0, 1, 1); SCENE.add(directionalLight); }
مصادر الضوء مختلفة. في أغلب الأحيان في مثل هذه المهام ، يتم استخدام الإضاءة المحيطة - ملء الضوء ، وضوء الاتجاه - في اتجاه معين. لا تزال هناك مصادر للضوء ، لكننا لسنا بحاجة إليها. نجعل لون التوهج أبيض بحيث لا توجد تشوهات.
قد يكون من المفيد اللعب بلون توهج التعبئة ، خاصةً بظلال الرمادي ، حتى تتمكن من إنشاء صورة أكثر نعومة.
الشيء المهم الثاني هو الكاميرا. هذا هو الكيان الذي يحدد النقطة التي نحن فيها والاتجاه الذي ننظر فيه.
function initCamera() { CAMERA = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000); CAMERA.position.z = 100; }
عادة ما يتم اختيار المعلمات الكاميرا عن طريق العين وتعتمد على النماذج المستخدمة.
الكائن الثالث الذي نحتاجه هو عارض. إنه مسؤول عن تقديم الصورة. التهيئة لها يتحدث عن نفسه:
function initRenderer() { RENDERER = new THREE.WebGLRenderer({ alpha: true }); RENDERER.setPixelRatio(window.devicePixelRatio); RENDERER.setSize(window.innerWidth, window.innerHeight); }
هناك حاجة إلى تحميل لتحميل البيانات من الأشكال المختلفة. هنا يمكنك العثور على قائمة طويلة من الخيارات ، لكننا نحتاج فقط إلى خيارين - واحد للصور (يأتي مع المجموعة) والآخر للطرز (قمنا بتوصيله في البداية).
function initLoaders() { LOADING_MANAGER = new THREE.LoadingManager(); IMAGE_LOADER = new THREE.ImageLoader(LOADING_MANAGER); OBJ_LOADER = new THREE.OBJLoader(LOADING_MANAGER); }
ننتقل إلى تحميل النموذج. كما هو متوقع ، يحدث بشكل غير متزامن. بعد تحميل النموذج ، يمكننا اللعب بمعلماته:
function loadModel() { OBJ_LOADER.load('./model.obj', (object) => { object.scale.x = 0.3; object.scale.y = 0.3; object.scale.z = 0.3; object.rotation.x = -Math.PI / 2; object.position.y = -30; OBJECT = object; SCENE.add(OBJECT); }); }
يبقى أن تبدأ عضو البرميل:
function animate() { requestAnimationFrame(animate); render(); } function render() { CAMERA.lookAt(SCENE.position); RENDERER.render(SCENE, CAMERA); }
نتيجة لذلك ، حصلنا فقط على شجرة عيد الميلاد بيضاء مع الظلال (أخذت النموذج من هنا ).

واحد ، اثنان ، ثلاثة ، شجرة عيد الميلاد تحترق! لكن بدون مواد ، بالطبع ، لن يحترق. نعم ، وسوف نتحدث عن ظلال النار وغيرها من العناصر في وقت آخر ... ولكن على الأقل يمكننا أن نرى أن نموذج شجرة عيد الميلاد هو "على التلفزيون".
قبل الانتقال إلى مواد ، من المفيد إضافة نافذة متصفح قياسية لتغيير حجم معالج الأحداث:
function initEventListeners() { window.addEventListener('resize', onWindowResize); onWindowResize(); } function onWindowResize() { CAMERA.aspect = window.innerWidth / window.innerHeight; CAMERA.updateProjectionMatrix(); RENDERER.setSize(window.innerWidth, window.innerHeight); }
إضافة الملمس
يعمل نموذجنا وملمس الصور على مبدأ المترجمين الملصقات على نماذج معدات الأطفال. كما نعلم بالفعل ، تتكون الكائنات في سياق WebGL من كومة من المثلثات. في حد ذاتها ، ليس لديهم لون. يوجد لكل "مثلث" نفس "الملصق" الثلاثي مع النسيج الذي تحتاج إلى التمسك به. ولكن إذا كان لدينا 1000 مثلث ، فسنحتاج إلى تحميل 1000 صورة نسيج؟ بالطبع لا. يتم إنشاء العفريت ، مثلها في ذلك مثل رموز CSS (ربما صادفتها في العمل) ، ومعلومات حول المثلثات وأين يتم إضافتها إلى النموذج نفسه. ثم Three.js بالفعل يفهم كل شيء بشكل مستقل ونرى النتيجة النهائية. في الواقع ، كل شيء أكثر تعقيدًا قليلاً ، لكن يجب فهم الفكرة بهذه الطريقة.
فروع شجرة عيد الميلاد ليست مثالا على ذلك. انهم جميعا نفس الشيء. سيتم رؤية بنية مثل هذا النسيج بشكل أفضل على مثال bulbasaur:

ولكن كلمات كافية ، دعونا ننكب على العمل. تهيئة الملمس وتحميل صورة معه:
function initTexture() { TEXTURE = new THREE.Texture(); } function loadTexture() { IMAGE_LOADER.load('./texture.jpg', (image) => { TEXTURE.image = image; TEXTURE.needsUpdate = true; }); }
الآن نحن بحاجة إلى توسيع وظيفة تحميل النموذج. إذا كان لدينا نفس نسيج bulbasaur ، فسيكون كل شيء بسيطًا. ولكن مع شجرة عيد الميلاد ، لا يغطي النسيج سوى الفروع. من الضروري فصلهم بطريقة أو بأخرى وتطبيقه فقط عليهم. كيف نفعل ذلك؟ يمكنك التعامل مع هذه المشكلة بطرق مختلفة. حان الوقت لاستخدام console.log وإلقاء نظرة على النموذج نفسه.
إذا كنت لا تعرف كيفية تمييز جزء معين من النموذج ، فاستخدم console.log. عادة ما تكون هذه هي أسرع طريقة لمعرفة كيف تختلف الأجزاء.
عادة ما يكون لدينا خياران لكيفية تقسيم النموذج إلى أجزاء. الأول (جيد) هو عندما يقوم الفنان ثلاثي الأبعاد بتوقيع الأجزاء المكونة للنموذج ولدينا حق الوصول إلى حقول أسماءهم ويمكننا أن نقرر منهم ما هو. في مثالنا ، هذا ليس كذلك ، ولكن هناك أسماء المواد. سوف نستخدمها. بالنسبة لأجزاء من النموذج من المادة "Christmas_Tree" ، سوف نستخدم الملمس:
function loadModel() { OBJ_LOADER.load('./model.obj', (object) => { object.traverse(function(child) { if (child instanceof THREE.Mesh) { switch (child.material.name) { case 'Christmas_Tree': child.material.map = TEXTURE; break;
لذلك نحن نحصل على شيء مثل هذا:

بالنسبة للأجزاء المصنوعة من مواد "حمراء" و "وردية" (هذه كرات - كرات عيد الميلاد) ، نضع ببساطة لونًا عشوائيًا. في مثل هذه الحالات ، من المريح استخدام HSL:
switch (child.material.name) { case 'Christmas_Tree': child.material.map = TEXTURE; break; case 'red': child.material.color.setHSL(Math.random(), 1, 0.5); break; case 'pink': child.material.color.setHSL(Math.random(), 1, 0.5); break; }
ملاحظة للفنانين: قم بإعطاء أسماء ذات معنى لكل شيء في النماذج. أسماء المواد في مثالنا ببساطة تحطيم الدماغ. أحمر هنا يمكن أن يكون أخضر. لم أغيرهم لإظهار العبث الكامل لما كان يحدث. الاسم التجريدي "مادة للكرات" سيكون أكثر عالمية.
إسقاط متماثل
الإسقاط المتساوي الكلمة المركبة في الترجمة إلى اللغة الروسية هو الإسقاط على قدم المساواة. ترجمت إلى الأسرة - سحبت الكرة على مستطيل. يمكنك اقتباس لي. لقد رأينا جميعًا خريطة للعالم في المدرسة - إنها مستطيلة ، لكننا نفهم أننا إذا حولناها قليلاً ، فسنحصل على كرة أرضية. هذا هو عليه. لفهم كيفية ترتيب هذه التشوهات بشكل أفضل ، ألق نظرة على الصورة:

عند إنشاء صور مصغرة لمنتجات مختلفة ، غالبًا ما يتم إنشاء الخلفية باستخدام مثل هذه التوقعات. نلتقط صورة مشوهة مع البيئة ونعرضها على كرة كبيرة. يبدو أن الكاميرا بداخلها. يبدو شيء مثل هذا:
function initWorld() { const sphere = new THREE.SphereGeometry(500, 64, 64); sphere.scale(-1, 1, 1); const texture = new THREE.Texture(); const material = new THREE.MeshBasicMaterial({ map: texture }); IMAGE_LOADER.load('./world.jpg', (image) => { texture.image = image; texture.needsUpdate = true; }); SCENE.add(new THREE.Mesh(sphere, material)); }
على سبيل المثال ، قوضت الحواف عن عمد ، لذلك إذا استخدمت المثال من جيثب ، فيمكنك العثور على خط التماس المميز الذي تغلق عليه الصورة. إذا كان أي شخص مهتمًا ، فسيتم أخذ أصله من هنا .
المجموع في الوقت الحالي لدينا شيء مثل هذا:

شجرة عيد الميلاد مع الكرات الملونة تبدو جميلة جدا.
ضوابط المدار
من أجل تقدير جمال غرفة ثلاثية الأبعاد ، أضف تحكمًا بالماوس. ثم يبدو أن كل شيء في وضع ثلاثي الأبعاد ، تحتاج إلى تحريف كل شيء. عادة ، يتم استخدام OrbitControls في مثل هذه المهام.
function initControls() { CONTROLS = new THREE.OrbitControls(CAMERA); CONTROLS.minPolarAngle = Math.PI * 1 / 4; CONTROLS.maxPolarAngle = Math.PI * 3 / 4; CONTROLS.update(); }
من الممكن وضع قيود على الزوايا التي يمكنك من خلالها تدوير الكاميرا ، وفرض قيود على التكبير وخيارات أخرى. من المفيد النظر إلى الوثائق ، فهناك الكثير من الأشياء المثيرة للاهتمام.
لا يمكنك إخبار الكثير عن خيار التحكم هذا. متصل ، قيد التشغيل ويعمل. فقط لا تنسى تحديث الحالة بانتظام:
function animate() { requestAnimationFrame(animate); CONTROLS.update(); render(); }
هنا يمكنك الحصول على شجرة مشتت قليلاً في عيد الميلاد في اتجاهات مختلفة ...
Raycaster
يسمح لك Raycaster بالقيام بما يلي: يرسم خطًا مستقيمًا في الفضاء ويجد جميع الكائنات التي يتقاطع معها. يتيح لك ذلك القيام بالكثير من الأشياء المثيرة للاهتمام المختلفة ، ولكن في سياق العروض التقديمية للمنتج ستكون هناك حالتان رئيسيتان - وهذا هو الرد على تحريك الماوس فوق شيء ما والرد على نقرة بالماوس على شيء ما. للقيام بذلك ، سوف تحتاج إلى رسم خطوط عمودي على الشاشة من خلال نقطة مع إحداثيات الماوس والبحث عن التقاطعات. هذا هو ما سنفعله. تمديد وظيفة التجسيد ، والبحث عن التقاطعات مع الكرات وإعادة رسمها:
function render() { RAYCASTER.setFromCamera(MOUSE, CAMERA); paintHoveredBalls();
مع حركات بسيطة للماوس ذهابًا وإيابًا ، نتأكد من أن كل شيء يعمل.

ولكن هناك دقة واحدة - Three.js لا يعرف كيفية تغيير الألوان بسلاسة. وبشكل عام ، هذه المكتبة لا تدور حول تغييرات سلسة في القيم. هذا هو الوقت المناسب للاتصال ببعض الأدوات المصممة لهذا الغرض ، على سبيل المثال Anime.js.
<script src='https://unpkg.com/animejs@2.2.0/anime.min.js'></script>
نستخدم هذه المكتبة لتحريك القيم:
switch (intersects[i].object.material.name) { case 'red':
الآن تتغير الألوان بسلاسة ، ولكن فقط بعد أن يتحرك الماوس بعيدًا عن الكرة. شيء يحتاج إلى إصلاح. سنستخدم رموزًا لهذا - فهي تسمح لنا بإضافة معلومات التعريف بأمان إلى الكائنات ، وسنحتاج فقط إلى إضافة معلومات حول ما إذا كانت الكرة متحركة أم لا.
تعد الرموز في ES6 + أداة قوية للغاية تتيح لك ، من بين أشياء أخرى ، إضافة معلومات إلى كائنات من مكتبات الطرف الثالث دون خوف من أن يؤدي ذلك إلى تعارض في الأسماء أو كسر المنطق.
نحن نصنع ثابتًا عالميًا (من الناحية النظرية ، فإن الأمر يستحق صنع كائن عالمي لجميع هذه الرموز ، لكن لدينا مثال بسيط ، لن نقوم بتعقيده):
const _IS_ANIMATED = Symbol('is animated');
ونضيف شيكًا إلى وظيفة إعادة طلاء الكرات:
if (!intersects[i].object[_IS_ANIMATED]) { anime({ targets: intersects[i].object.material.color, r: 0, g: 0, b: 0, easing: 'easeInOutQuad' }); intersects[i].object[_IS_ANIMATED] = true; }
الآن هم إعادة رسم بسلاسة على الفور تحوم. وبالتالي ، بمساعدة الرموز ، يمكنك إضافة اختبارات مماثلة في الرسوم المتحركة بسرعة دون حفظ حالات جميع الكرات في مكان منفصل.
تلميحات الأدوات
آخر شيء نفعله اليوم هو تلميحات الأدوات. كثيرا ما تواجه هذه المهمة. بالنسبة للمبتدئين ، نحتاج فقط إلى تعويضهم.
<div class='popup-3d'> !</div>
.popup-3d { color: #fff; font-family: 'Pacifico', cursive; font-size: 10rem; pointer-events: none; }
تذكر تعطيل أحداث المؤشر إذا كنت لا تحتاج إليها.
يبقى لإضافة CSS3DRenderer. هذا في الواقع ليس عارضًا تمامًا ، بل هو شيء يضيف ببساطة تحويلات CSS إلى العناصر ويبدو أنها في مشهد مشترك. بالنسبة إلى الملصقات المنبثقة - هذا ما تحتاجه فقط. نحن نصنع المتغير الشامل CSSRENDERER ، وننشئه ولا ننسى استدعاء وظيفة التجسيد نفسها. كل شيء يبدو وكأنه عارض منتظم:
function initCSSRenderer() { CSSRENDERER = new THREE.CSS3DRenderer(); CSSRENDERER.setSize(window.innerWidth, window.innerHeight); CSSRENDERER.domElement.style.position = 'absolute'; CSSRENDERER.domElement.style.top = 0; } function render() { CAMERA.lookAt(SCENE.position); RENDERER.render(SCENE, CAMERA); CSSRENDERER.render(SCENE, CAMERA); }
لم يحدث شيء في الوقت الحالي. في الواقع ، لم نفعل شيئا. نهيئ العنصر المنبثق ، يمكننا أن نلعب على الفور مع حجمها وموقعها في الفضاء:
function initPopups() { const popupSource = document.querySelector('.popup-3d'); const popup = new THREE.CSS3DObject(popupSource); popup.position.x = 0; popup.position.y = -10; popup.position.z = 30; popup.scale.x = 0.05; popup.scale.y = 0.05; popup.scale.z = 0.05; console.log(popup); SCENE.add(popup); }
الآن نرى النقش "ثلاثي الأبعاد". في الواقع ، ليس في وضع ثلاثي الأبعاد بالكامل ، فهو يقع في مقدمة اللوحة القماشية ، ولكن بالنسبة للنصائح المنبثقة ، فهي ليست مهمة جدًا ، والتأثير مهم
تظل آخر لمسة - لإظهار النقش بسلاسة في مجموعة معينة من الزوايا. مرة أخرى ، استخدم الرمز العالمي:
const _IS_VISIBLE = Symbol('is visible');
ونقوم بتحديث حالة العنصر المنبثق حسب زاوية دوران الكاميرا:
function updatePopups() { const popupSource = document.querySelector('.popup-3d'); const angle = CONTROLS.getAzimuthalAngle(); if (Math.abs(angle) > .9 && popupSource[_IS_VISIBLE]) { anime({ targets: popupSource, opacity: 0, easing: 'easeInOutQuad' }); popupSource[_IS_VISIBLE] = false; } else if (Math.abs(angle) < .9 && !popupSource[_IS_VISIBLE]) { anime({ targets: popupSource, opacity: 1, easing: 'easeInOutQuad' }); popupSource[_IS_VISIBLE] = true; } }
كل شيء بسيط جدا. الآن يظهر النقش بسلاسة ويختفي. يمكنك إضافة التدوير التلقائي والاستمتاع بالنتيجة.
CONTROLS.autoRotate = true; CONTROLS.autoRotateSpeed = -1.0;

الخاتمة
نظرنا اليوم في كيفية عرض النماذج ثلاثية الأبعاد على صفحتنا ، وكيفية تحويلها بالماوس ، وكيفية صنع تلميحات الأدوات ، وكيفية الاستجابة لتحوم الماوس فوق أجزاء معينة من النموذج ، وكيفية استخدام الأحرف في سياق الرسوم المتحركة المختلفة. نأمل أن تكون هذه المعلومات مفيدة. حسنًا ، كل ما سيحدث الآن ، أنت تعرف الآن ما يمكنك تعلمه خلال العطلات.
ملاحظة: المصادر الكاملة للمثال متعرجة متاحة على جيثب .