لطفا صبر کنید ...

ساخت اشکال تعاملی با Three.js

الهام غایب

توسط الهام غایب

دیدگاه ها: 0
بازدید ها : 129
رایگان
هدف ما این است که شما، در بالاترین سطح طراحی و توسعه وب باشید.

ثبت نام کنید

در این مقاله از لیداوب، خواهید آموخت که چگونه با استفاده از Three.js اشکال مختلف ساخته و در روشی کارامد، آن‌ها را وادار کنید تا به ورودی ماوس و لمس واکنش نشان دهند. 


برای شروع باید تجربه کار با Three.js را در سطح متوسط داشته باشید. ما برای اختصار بخشی از کدها را حذف می‌کنیم چون فرض بر این است که شما می‌دانید چگونه یک صفحه Three.js راه اندازی کرده و shader‌ها را ایمپورت کنید.

ساخت اشکال تعاملی با Three.js

اشکال هندسی نمونه

اشکال یا ذرات براساس پیکسل‌های یک تصویر ایجاد می‌شوند. ابعاد تصویر ما ۱۸۰x۳۲۰ یا ۵۷۶۰۰ پیکسل است. البته ما نیازی به ایجاد یک شکل هندسی برای هر ذره نداریم. می‌توانیم تنها یک شکل هندسی ایجاد کرده و آن را ۵۷۶۰۰ مرتبه با پارامترهای مختلف رندر کنیم. به این کار نمونه سازی هندسی گفته می‌شود. با Three.js ما از InstancedBufferGeometry برای تعریف شکل هندسی، BufferAttribute برای خصوصیاتی که برای هر نمونه بدون تغییر باقی می‌مانند و InstancedBufferAttribute برای خصوصیاتی مثل رنگ و اندازه که می‌توانند بین نمونه‌ها متفاوت باشند، استفاده می‌کنیم. شکل هندسی ذرات ما یک چهار ضلعی ساده است که از ۴ راس و دو مثلث تشکیل شده است:

const geometry = new THREE.InstancedBufferGeometry();

// positions
const positions = new THREE.BufferAttribute(new Float32Array(4 * 3), 3);
positions.setXYZ(0, -0.5, 0.5, 0.0);
positions.setXYZ(1, 0.5, 0.5, 0.0);
positions.setXYZ(2, -0.5, -0.5, 0.0);
positions.setXYZ(3, 0.5, -0.5, 0.0);
geometry.addAttribute('position', positions);

// uvs
const uvs = new THREE.BufferAttribute(new Float32Array(4 * 2), 2);
uvs.setXYZ(0, 0.0, 0.0);
uvs.setXYZ(1, 1.0, 0.0);
uvs.setXYZ(2, 0.0, 1.0);
uvs.setXYZ(3, 1.0, 1.0);
geometry.addAttribute('uv', uvs);

// index
geometry.setIndex(new THREE.BufferAttribute(new Uint16Array([ 0, 2, 1, 2, 3, 1 ]), 1));

سپس به میان پیکسل‌های تصویر می‌چرخیم و ویژگی‌های نمونه را اعمال می‌کنیم. به دلیل اینکه کلمه position از قبل گرفته شده است ما از کلمه offset برای ذخیره جای هر نمونه استفاده می‌کنیم که x و y هر پیکسل در تصویر است. همچنین، می‌خواهیم شاخص ذره و یک زاویه تصادفی که بعدا برای انیمیشن به کار می‌رود را ذخیره کنیم:

const indices = new Uint16Array(this.numPoints);
const offsets = new Float32Array(this.numPoints * 3);
const angles = new Float32Array(this.numPoints);

for (let i = 0; i < this.numPoints; i++) {
	offsets[i * 3 + 0] = i % this.width;
	offsets[i * 3 + 1] = Math.floor(i / this.width);

	indices[i] = i;

	angles[i] = Math.random() * Math.PI;
}

geometry.addAttribute('pindex', new THREE.InstancedBufferAttribute(indices, 1, false));
geometry.addAttribute('offset', new THREE.InstancedBufferAttribute(offsets, 3, false));
geometry.addAttribute('angle', new THREE.InstancedBufferAttribute(angles, 1, false));

مواد تشکیل دهنده ذرات

این مواد شامل یک RawShaderMaterial با سایه ساز particle.vert و particle.frag است. متحدالشکل‌ها به این صورت تعریف می‌شوند:

uTime‌: زمان سپری شده، فریم به روز رسانی شده

uRandom: عامل تصادفی بودن که برای جا به جایی ذرات در محور x و y به کار می‌رود

uDepth: ماکزیمم نوسان ذرات در z

 uSize: سایز اولیه ذرات

 uTexture: بافت عکس

 uTextureSize: ابعاد بافت تصویر

 uTouch: بافت لمسی

const uniforms = {
	uTime: { value: 0 },
	uRandom: { value: 1.0 },
	uDepth: { value: 2.0 },
	uSize: { value: 0.0 },
	uTextureSize: { value: new THREE.Vector2(this.width, this.height) },
	uTexture: { value: this.texture },
	uTouch: { value: null }
};

const material = new THREE.RawShaderMaterial({
	uniforms,
	vertexShader: glslify(require('../../../shaders/particle.vert')),
	fragmentShader: glslify(require('../../../shaders/particle.frag')),
	depthTest: false,
	transparent: true
});

یک vertex shader ساده، جای ذرات را مستقیما بر اساس خصوصیت offset آن‌ها مشخص می‌کند. برای جالب‌تر کردن کارها با استفاده از random و noise، ذرات را جا به جا می‌کنیم. همچنین، همین عمل برای اندازه ذرات هم انجام می‌شود:

// particle.vert

void main() {
	// displacement
	vec3 displaced = offset;
	// randomise
	displaced.xy += vec2(random(pindex) - 0.5, random(offset.x + pindex) - 0.5) * uRandom;
	float rndz = (random(pindex) + snoise_1_2(vec2(pindex * 0.1, uTime * 0.1)));
	displaced.z += rndz * (random(pindex) * 2.0 * uDepth);

	// particle size
	float psize = (snoise_1_2(vec2(uTime, pindex) * 0.5) + 2.0);
	psize *= max(grey, 0.2);
	psize *= uSize;

	// (...)
}

سایه انداز تکه‌ای رنگ‌های RGB را از تصویر اصلی نمونه برداری کرده و آن را با استفاده متد luminosity به مقیاس خاکستری تبدیل می‌کند (0.21 R + 0.72 G + 0.07 B). کانال آلفا با فاصله خطی به مرکز UV تعیین می‌شود که اصولا یک دایره ایجاد می‌کند. مرز دایره می‌تواند با استفاده از smoothstep ، بلور شود:

// particle.frag

void main() {
	// pixel color
	vec4 colA = texture2D(uTexture, puv);

	// greyscale
	float grey = colA.r * 0.21 + colA.g * 0.71 + colA.b * 0.07;
	vec4 colB = vec4(grey, grey, grey, 1.0);

	// circle
	float border = 0.3;
	float radius = 0.5;
	float dist = radius - distance(uv, vec2(0.5));
	float t = smoothstep(0.0, border, dist);

	// final color
	color = colB;
	color.a = t;

	// (...)
}

بهینه سازی

ما در این آموزش، اندازه ذرات را براساس روشنایی آن‌ها تعیین کردیم یعنی ذرات تیره تقریبا نامرئی هستند. این کار باعث می‌شود جا برای بهینه سازی باز شود. وقتی در داخل پیکسل‌های تصویر گردش می‌کنیم، می‌توانیم آن‌هایی که زیادی تیره هستند را نادیده بگیریم که این باعث کاهش تعداد ذرات و افزایش عملکرد خواهد شد. بهینه سازی پیش از آنکه ما InstancedBufferGeometry را ایجاد کنیم، شروع می‌شود. ما یک canvas موقت ایجاد کرده، تصویر را روی آن رسم کرده و getImageData() را برای بازیابی آرایه‌ای از رنگ‌ها [R, G, B, A, R, G, B … ] فراخوانی می‌کنیم. سپس، یک آستانه (hex #22 دسیمال ۳۴) تعیین کرده و آن را در برابر کانال قرمز آزمایش می‌کنیم. کانال قرمز یک انتخاب اختیاری است ما می‌توانیم از کانال‌های آبی، سبز یا حتی میانگین این سه کانال استفاده کنیم اما استفاده از کانال قرمز آسان‌تر است:

// discard pixels darker than threshold #22
if (discard) {
	numVisible = 0;
	threshold = 34;

	const img = this.texture.image;
	const canvas = document.createElement('canvas');
	const ctx = canvas.getContext('2d');

	canvas.width = this.width;
	canvas.height = this.height;
	ctx.scale(1, -1); // flip y
	ctx.drawImage(img, 0, 0, this.width, this.height * -1);

	const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
	originalColors = Float32Array.from(imgData.data);

	for (let i = 0; i < this.numPoints; i++) {
		if (originalColors[i * 4 + 0] > threshold) numVisible++;
	}
}

همچنین، باید حلقه‌ای که offset ،angle و pindex را در آن تعریف کردیم به روز رسانی کنیم تا آستانه را در نظر بگیرد:

for (let i = 0, j = 0; i < this.numPoints; i++) {
	if (originalColors[i * 4 + 0] <= threshold) continue;

	offsets[j * 3 + 0] = i % this.width;
	offsets[j * 3 + 1] = Math.floor(i / this.width);

	indices[j] = i;

	angles[j] = Math.random() * Math.PI;

	j++;
}

تعامل

روش‌های متفاوت بسیاری برای تعامل با ذرات وجود دارد. مثلا می‌توانیم به هر ذره یک خصوصیت شتاب داده و بر‌اساس نزدیکی آن به مرورگر ماوس در هر فریم آن را به روز رسانی کنیم. این یک روش کلاسیک بوده و به خوبی عمل می‌کند اما اگر بخواهیم بین ده‌ها هزار ذره گردش کنیم، کار سنگینی خواهد بود. یک روش کارامدتر این است این کار را درون یک shader انجام دهیم. می‌توانیم جایگاه نشانگر ماوس را به عنوان یک یونیفرم در نظر گرفته و ذرات را بر‌اساس فاصله از آن، جا به جا کرد. گرچه، این روش بسیار سریع‌تر است اما نتیجه می‌تواند کاملا سخت باشد. ذرات به جایگاه تعیین شده رفته و نمی‌توانند به آسانی به آن وارد یا خارج شوند.

روش منتخب

تکنیک مورد نظر ما کشیدن نشانگر ماوس در یک بافت بود. مزیت این روش در آن است که می‌توانیم تاریخچه‌ای از جایگاه‌های نشانگر نگه داشته و یک دنباله ایجاد کنیم. همچنین، می‌توانیم یک تابع آسان برای شعاع دنباله به کار ببریم تا باز شدن و جمع شدن آن به آسانی انجام شود. هر اتفاقی که در shader رخ دهد، به موازات آن برای تمام ذرات اعمال خواهد شد. برای آنکه جایگاه نشانگر ماوس را به دست آوریم از یک Raycaster و یک PlaneBufferGeometry  ساده هم اندازه شکل هندسی اصلی خود استفاده می‌کنیم. تعامل در Three.js موضوع مهمی است، لطفا این مثال را مشاهده کنید: https://threejs.org/examples/#webgl_interactive_cubes

وقتی یک سطح مقطع بین نشانگر ماوس و صفحه وجود داشته باشد، می‌توانیم از مختصات UV در داده‌های سطح مقطع استفاده کرده تا جایگاه نشانگر ماوس را به دست آوریم. سپس، جایگاه‌ها در یک آرایه ذخیره شده و به یک canvas کشیده می‌شوند. Canvas از طریق یونیفرم uTouch به عنوان یک بافت به shader اضافه می‌شود. در vertex shader ذرات بر اساس روشنایی پیکسل‌ها در بافت لمسی، جا به جا می‌شوند:

// particle.vert

void main() {
	// (...)

	// touch
	float t = texture2D(uTouch, puv).r;
	displaced.z += t * 20.0 * rndz;
	displaced.x += cos(angle) * t * 20.0 * rndz;
	displaced.y += sin(angle) * t * 20.0 * rndz;

	// (...)
}

مطالعه مقالات بیشتر در لیداوب:

امیدواریم از این آموزش نیز لذت برده باشید. با سایر آموزش‌های طراحی سایت و جاوا اسکریپت ما همراه باشید. می‌توانید سوالات و نظرات خود را در زیر همین مقاله در لیداوب، با ما به اشتراک بگذارید.

منبع :

5 از 1 رای

 مطالب مرتبط  

در قسمت زیر مطالبی وجود دارند که با مقاله فعلی مرتبط هستند



متاسفانه فقط اعضای سایت قادر به ثبت دیدگاه هستند

برترین مطالب

آموزش در لیداوب

از مقالات و ویدیو های آموزشی خودتان کسب درآمد کنید!

جدیدترین سرویس ما برای دوستان متخصص و خوش بیان در زمینه طراحی وب ارائه شد. این سرویس به شما این امکان را می دهد که به آسانی مقالات و یا ویدیو های آموزشی خود را در لیداوب منتشر کنید. فرصت مناسبی است که بدون داشتن وب سایت و یا وبلاگ و مهم تر از همه بدون هزینه، مطالب خود را در لیداوب منتشر کنید و مهم تر از همه با فروش آنها کسب درآمد کنید. همین حالا شروع کنید و این فرصت استثنایی را از دست ندهید.