اذهب إلى المحتوى

سنشرح في هذا المقال كيفية بناء محاكي مرئي موسيقى باستخدام Three.js والتعامل مع تقنيات مزامنة الصوت مؤثرات مرئية ثلاثية الأبعاد. ما سنحاول بناءه في هذا المقال هو صور متفاعلة ديناميكيًا مع الصوت بشكل قريب من الفكرة الواردة في  هاذا المشروع.

001 صورة متفاعلة مع الصوت

تهيئة المشروع

في البداية سننشئ مشروع جديد بأي محرر أكواد نفضله، ونضيف له مكتبة three.js، ثم ننشئ ملف جافا سكريبت ونضع فيه الكود التالي الذي يضمن أن الصوت سيعمل فقط عندما يطلب المستخدم ذلك وينقر في الصفحة، وذلك بهدف تجنب مشكلات تشغيل الصوت التلقائي التي وضعتها المتصفحات الأساسية وضمان تجربة مستخدم أفضل.

 export default class App {
  constructor() {
    this.onClickBinder = () => this.init()
    document.addEventListener('click', this.onClickBinder)
  }

  init() {
    document.removeEventListener('click', this.onClickBinder)

       مشهد Three.js بسيط//    
    this.renderer = new THREE.WebGLRenderer()
    this.camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 10000)
    this.scene = new THREE.Scene()
  }
}

تحليل البيانات الصوتية

سنهيئ الآن مدير الصوت (Audio Manager) ومدير الإيقاع (BPM Manager) لتحميل الصوت، وتحليله، ومزامنته مع العناصر المرئية. وسنبدأ تشغيلهما باستخدام async و async.

async createManagers() {
    App.audioManager = new AudioManager()
  await App.audioManager.loadAudioBuffer()

  App.bpmManager = new BPMManager()
  App.bpmManager.addEventListener('beat', () => {
    this.particles.onBPMBeat()
  })
  await App.bpmManager.detectBPM(App.audioManager.audio.buffer)
}

ملاحظة: يشير عدد النبضات في الدقيقة Beats Per Minute -أو BPM اختصارًا- إلى سرعة الإيقاع الموسيقي.

سيحمّل الصنف AudioManager في الكود السابق الصوت من عنوان URL محدد، وفي هذا المقال اخترنا ملف صوتي من موقع Spotify، بعد تحميل الصوت سنحلله ثم نقسّم إشارات الصوت إلى نطاقات ترددية في الوقت الفعلي.

const audioLoader = new THREE.AudioLoader();
audioLoader.load(this.song.url, buffer => {
    this.audio.setBuffer(buffer);
})

بيانات التردد

علينا فصل نطاق الترددات إلى الترددات المنخفضة والمتوسطة والعالية لحساب السعة amplitude.

002 نطاق الترددات

سنقسم نطاقات الترددات إلى frequency bands كما يلي:

  • ترددات منخفضة Low
  • ترددات متوسطة Mid
  • ترددات عالية High

كل مجموعة لها نطاق ترددات محدد، مثلًا تبدأ الترددات المنخفضة من lowFrequency وتنتهي عند بداية midFrequency. وتبدأ الترددات العالية من midFrequency وتنتهي عند highFrequency.

ولحساب متوسط السعة داخل نطاق معيّن يمكننا ضرب الترددات بالقيمة Buffer length والتيتمثل عدد العينات الصوتية المخزنة في Buffer، ثم نقسم الناتج على معدل أخذ العينات sample rate، وهو عدد العيّنات التي يتم التقاطها في الثانية فهذا يساعدنا على تحديد الموضع الصحيح داخل المصفوفة التي تحتوي على بيانات الترددات الصوتية بناءً على النطاق الترددي. بعد حساب المتوسط، نحول القيمة الناتجة إلى نطاق يتراوح بين 0 و 1، حتى نتمكن من استخدامها بشكل مرن في التطبيقات الرسومية أو الصوتية.

this.lowFrequency = 10;
this.frequencyArray = this.audioAnalyser.getFrequencyData();
const lowFreqRangeStart = Math.floor((this.lowFrequency * this.bufferLength) / this.audioContext.sampleRate)
const lowFreqRangeEnd = Math.floor((this.midFrequency * this.bufferLength) / this.audioContext.sampleRate)
const lowAvg = this.normalizeValue(this.calculateAverage(this.frequencyArray, lowFreqRangeStart, lowFreqRangeEnd));

نفس الطريقة لنطاق الترددات المرتفعة بين MID وHIGH//

كشف الإيقاع باستخدام Web Audio Beat Detector

حسبنا في الفقرة السابقة قيمة سعة الترددات لكنها لا تكفي لوحدها لربط إيقاعات الموسيقى بالتأثيرات المرئية، سنحتاج لمعرفة قيمة الإيقاع BPM أي عدد النبضات في الدقيقة فهذه القيمة أساسية لجعل العناصر تتفاعل بشكل متزامن مع نبضات الموسيقى. لحسابها سنعتمد على استخدام وحدة جافا سكريبت‏ مفتوحة المصدر باسم web audio beat detector حيث نحتاج لتمرير القيمة audioBuffer لهذه الوحدة وحساب الإيقاع بشكل غير متزامن كما يلي:

const { bpm } = await guess(audioBuffer);

إرسال الإشارات

بعد أن كشفنا قيمة الإيقاع BPM يمكننا إرسال إشارة بوقوع حدث بشكل متكرر عند كل نبضة إيقاع باستخدام الدالة setInterval.

this.interval = 60000 / bpm; // Convert BPM to interval
this.intervalId = setInterval(() => {
    this.dispatchEvent({ type: 'beat' })
}, this.interval);

إنشاء الأشكال المرئية المتفاعلة ديناميكيًا مع الصوت

سننشئ الآن أشكال لجزيئات ديناميكية لتتفاعل مع الصوت. سنبدأ بتعريف دالتين لإنشاء أشكال هندسية بسيطة، سننشئ مكعب Box وأسطوانة Cylinder بأجزاء وخواص عشوائية لتكون الأشكال الأساسية للجزئيات، سينتج عن هذه الطريقة هيكل مميز في كل مرة.

ثم سنضيف هاتين الدالتين لكائن Three.Points مع ShaderMaterial بسيط.

const geometry = new THREE.BoxGeometry(1, 1, 1, widthSeg, heightSeg, depthSeg)
const material = new THREE.ShaderMaterial({
  side: THREE.DoubleSide,
  vertexShader: vertex,
  fragmentShader: fragment,
  transparent: true,
  uniforms: {
    size: { value: 2 },
  },
})
const pointsMesh = new THREE.Points(geometry, material)

يمكننا الآن إنشاء تمثيلات الهندسية بسمات عشوائية بفترة زمنية محددة، لاحظ الشكل التالي:

توضيح مفهوم Vertex Shader و Fragment Shader

سنستخدم في فقراتنا التالية مفهومي مظلل الرؤوس Vertex Shader ومظلل الشرائح Fragment Shader في مكتبة Three.js لتخصيص وتعديل مظهر الكائنات الرسومية ثلاثية الأبعاد. فمظلل الرؤوس Vertex Shader هو جزء من الكود يُنفَّذ لكل رأس Vertex في الكائن ثلاثي الأبعاد ونستخدمه لتعديل موقع الرأس بناءً على المعاملات المرسلة من جافا سكريبت، وتغيير شكل الكائن عن طريق تعديل مواقع الرؤوس كتحريكها أو تدويرها ولكن لا يمكن من خلاله تعديل ألوان البكسلات أو التفاصيل الدقيقة الأخرى للكائن.

من ناحية أخرى، يعمل مظلل الشرائح Fragment Shader على تحديد لون كل جزء صغير أو بكسل في الكائن ثلاثي الأبعاد، وهو يحدد اللون النهائي لكل بكسل بناءً على القيم المرسلة من Vertex Shader. ويمكن ضمنه تطبيق مؤثرات لونية على البكسلات كتغيير الألوان أو إضافة تأثيرات مثل الإضاءة أو الظلال. ومن خلال دمج Vertex Shader و Fragment Shader يمكن تحديد مظهر الكائن، حيث يُسمح بتخصيص الشكل والألوان بشكل مرن مما يوفر إمكانيات غير محدودة لإبداع مؤثرات مرئية مميزة.

تحريك جزئيات الشكل عشوائيًا

سندرج ضوضاء دورانية Curl Noise للمظلل vertex shader لتحريك جزئيات الشكل بحركات طبيعية لها وإضافة حركات دوامية وسلسة لها. حيث سنحرك هذه الجزيئات وننشئ تأثيرات ديناميكية تتحكم بمظهرها وسلوكها. سنبدأ بتحديد موقع هدف نسميه newpos لكل نقطة، ثم نضيف ضوضاء دورانية Cur Noise على طول الشعاع الطبيعي لها ونغير هذه الضوضاء اعتمادا على التردد frequency والسعة amplitude، سيعدل تأثير الضوضاء اللولبية بناءً على المسافة بين النقطة الحالية التي نحركها والهدف d وينتج تحرك سلس يتلاشى تدريجيًا بمعنى أن التغيير يعتمد على المسافة، سيكون التأثير أكبر عندما تكون المسافة صغيرة، ويتناقص تدريجيًا كلما اقتربت النقطة من الهدف بسبب تداخل المسافة.

vec3 newpos = position;
vec3 target = position + (normal * .1) + curl(newpos.x * frequency, newpos.y * frequency, newpos.z * frequency) * amplitude;
float d = length(newpos - target) / maxDistance;
newpos = mix(position, target, pow(d, 4.));

سنضيف أيضا حركة متموجة إلى newpos.z لإضافة مزيد من الحيوية للنقاط المتحركة.

newpos.z += sin(time) * (.1 * offsetGain);

علاوة على ذلك يتغير حجم كل نقطة ديناميكيًا بناءً على مدى قرب النقطة من الهدف وعمقها في المشهد، مما يجعل حركة النقاط تبدو ثلاثية أبعاد أكثر.

gl_PointSize = size + (pow(d,3.) * offsetSize) * (1./-mvPosition.z);

ستبدو النتيجة النهائية للحركة العشوائية كالتالي:

إضافة الألوان

سنخفي النقطة في مظلل الشرائح fragment shader باستخدام دالة تنشئ شكل دائري ثم نحدد تدرج لوني بين اللونين startColor و endColor بناءً على المسافة vDistance بين النقطة والهدف والتي حسبناها في المظلل vertex shader. هذا يعني أن الألوان ستتغير تدريجيًا حسب المسافة أيضًا، مما ينتج تأثيرًا مرئيًا سلسًا عند تحرك النقطة.

vec3 circ = vec3(circle(uv,1.));
vec3 color = mix(startColor,endColor,vDistance);
gl_FragColor=vec4(color,circ.r * vDistance);

تظهر النتيجة على النحو التالي:

دمج الصوت مع المؤثرات المرئية

سنستخدم الآن إبداعنا لتخصيص بيانات الصوت والإيقاع لكل الخصائص التي نريد تغييرها، سواء في مظلل الرؤوس الذي يتحكم في الشكل والموقع أو في مظلل الشرائح الذي يتحكم في الألوان والتأثيرات. يمكننا أيضًا إضافة بعض الرسوم المتحركة العشوائية لتغيير الحجم والموقع position، والدوران باستخدام مكتبة GSAP، وهي مكتبة تساعد في جعل الأشياء تتحرك وتتغير بسلاسة في الوقت الحقيقي.

update() {
  // تحديث السعة ديناميكيًا بناءً على بيانات التردد العالي من مدير الصوت
  this.material.uniforms.amplitude.value = 0.8 + THREE.MathUtils.mapLinear(App.audioManager.frequencyData.high, 0, 0.6, -0.1, 0.2)

  // تحديث الإزاحة بناءً على بيانات التردد المنخفض لتغييرات تأثيرات طفيفة
  this.material.uniforms.offsetGain.value = App.audioManager.frequencyData.mid * 0.6

  // تحويل بيانات التردد المنخفض إلى نطاق معين واستخدامه لزيادة قيمة الوقت
  const t = THREE.MathUtils.mapLinear(App.audioManager.frequencyData.low, 0.6, 1, 0.2, 0.5)
  this.time += THREE.MathUtils.clamp(t, 0.2, 0.5) // تقييد القيمة للتأكد من بقائها ضمن النطاق المطلوب

  this.material.uniforms.time.value = this.time
}

الخلاصة

شرحنا في هذا المقال كيفية إنشاء تأثيرات صوتية مزامنة للإيقاع باستخدام مكتبة Three.js. يمكنكم بعد قراءة هذا المقال استخدام Three.js لتحريك الجزيئات بطريقة ديناميكية و مزامنة الصوت وإنشاء مؤثرات مرئية مزامنة للإيقاع والتحكم بها.

ترجمة -وبتصرف- لمقال Creating Audio-Reactive Visuals with Dynamic Particles in Three.js لصاحبه Tiago Canzian.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...