ساخت یک جلوه لنز مربعی دنبال ماوس با Three.js و GLSL

یادداشت سردبیر: ما خوشحالیم که Tomoyuki Nakata، توسعه دهنده خلاق در baqemono، به جشن کوچک Three.js ما قبل از اولین کنفرانس Three.js در پاریس می پیوندد. به عنوان بخشی از ماراتن Three.js ما، او این افکت لنز فوقالعاده ماوس را که با Three.js و GLSL ایجاد شده است را برای ما به ارمغان میآورد. ما خوشحالیم که او را برای سوار شدن همراهی می کنیم! لذت ببرید!
🇫🇷 صبر کنید… هنوز بلیط خود را ندارید؟ اولین کنفرانس Three.js به پاریس می آید و بلیط ها برای همیشه منتظر نمی مانند. از یک کد استفاده کنید CODROPS برای 15 درصد تخفیف و بلیط خود را قبل از فروش دریافت کنید →
ایده
وقتی یک تصویر در مقیاس خاکستری، یک تصویر رنگی و یک افکت عدسی که ماوس را دنبال می کند، ترکیب می کنید، چه اتفاقی می افتد؟ در این آموزش، ما با استفاده از Three.js و GLSL یک جلوه لنز مربعی دنبال ماوس می سازیم.
این افکت دو تصویر را لایه بندی می کند: یک تصویر در مقیاس خاکستری صفحه را می پوشاند، در حالی که یک تصویر رنگی از طریق یک ناحیه مربعی که نشانگر ماوس را دنبال می کند نشان داده می شود. در داخل این مربع، اعوجاج لنز و جابجایی شعاعی RGB را اضافه می کنیم تا زمانی که تصویر در مقیاس خاکستری موج ظریف و حرکت نویز مانند خود را داشته باشد.
اگرچه نتیجه نهایی بسیار پیچیده به نظر می رسد، اثر از تعداد کمی از قطعات ساده ساخته شده است. ما ماسک مربعی را ایجاد می کنیم، بدون توجه به اندازه نما، آن را مربع نگه می داریم، جلوه های لنز و تغییر RGB را در سایه بان قطعه اعمال می کنیم و مربع را همانطور که ماوس را دنبال می کند به آرامی متحرک می کنیم. ما همچنین یک رابط کاربری گرافیکی اضافه خواهیم کرد تا پارامترهای مختلف را بتوان در زمان واقعی تنظیم کرد.
ایده این پروژه زمانی مطرح شد که من در حال مرور Pinterest بودم و طرحی را پیدا کردم که در آن بخشی از یک تصویر در مقیاس خاکستری به نظر می رسد از یک مربع رنگی با جلوه لنز بریده شده است. مرجع اصلی را می توانید اینجا ببینید. به نظر میرسید که میتوان آن را با WebGL بازسازی کرد، و من فکر میکردم که اگر مربع از ماوس پیروی کند و تصویر مقیاس خاکستری در پیشزمینه جلوهای جداگانه داشته باشد، نتیجه جالبتر خواهد بود. بنابراین تصمیم گرفتم آن را با Three.js و GLSL بازسازی کنم.
ما چه می سازیم؟
اثر نهایی شامل عناصر زیر است:
- تصویر در مقیاس خاکستری که کل صفحه را می پوشاند
- تصویر رنگی فقط در داخل مربع قابل مشاهده است
- یک اعوجاج به سبک لنز CC که از مرکز مربع بیرون میزند
- یک افست RGB که به سمت لبه های مربع قوی تر می شود
- یک ماسک مربعی که بدون توجه به اندازه صفحه نمایش، مربع باقی می ماند
- تعامل صاف ماوس با تاخیر جزئی
- شکل موج و اعوجاج تصادفی روی تصویر خاکستری اعمال می شود
- رابط کاربری گرافیکی برای تنظیم پارامترها در زمان واقعی
اگرچه نتیجه ممکن است کمی پیچیده به نظر برسد، اما از پس پردازش برای اهداف رندر یا هیچ مدل سه بعدی استفاده نمی کند. در عوض، با ترکیب عناصر بالا، کل افکت را در یک قطعه سایه زن ایجاد می کنیم.
ساختار پروژه
پرونده ها و نقش آنها به شرح زیر سازماندهی شده است:
src/
└── scripts/
├── webgl/
│ ├── glsl/
│ │ ├── chunks/
│ │ │ ├── ccLens.glsl
│ │ │ ├── coverUv.glsl
│ │ │ └── random3.glsl
│ │ ├── frag/
│ │ │ └── frag.glsl
│ │ └── vert/
│ │ └── vert.glsl
│ ├── mesh/
│ │ └── Mesh.ts
│ ├── stage/
│ │ └── Stage.ts
│ └── Webgl.ts
└── index.ts
glsl:chunksدارای ویژگی های قابل استفاده مجدد است،fragشامل سایه زن های قطعه وvertشامل سایه زن های راس است.mesh(Mesh): اندازه پنجره، بارگذاری بافت، ایجاد مش، اندازه مش، به روز رسانی های یکپارچه و کارهای مرتبط را مدیریت می کند.stage(Stage): صحنه، اندازه صحنه، ایجاد دوربین و رندر و به روز رسانی آنها را مدیریت می کند.Webgl: ایجاد و مقداردهی اولیه می کندMeshوStageکلاس ها، رابط کاربری گرافیکی را متصل می کند، رویدادها را ثبت می کند و حلقه رندر را مدیریت می کند.
ساختار کلاس کاملاً متعارف است، بنابراین این مقاله عمدتاً بر روی اجرای قطعه سایه زن متمرکز است.
تنظیم صحنه
بیایید با شروع کنیم Stage کلاس این کلاس یک صحنه، دوربین، رندر و سایر عناصر اساسی برای کار با Three.js ایجاد می کند و رندرینگ و تغییر اندازه را تنظیم می کند. بسیاری از این موارد استاندارد هستند، اما یکی از جزئیاتی که ارزش توضیح دارد نحوه تنظیم موقعیت Z دوربین است.
موقعیت Z دوربین را تنظیم کنید
موقعیت Z دوربین با استفاده از عملکردی به نام تنظیم می شود calcViewportDistance.
const calcViewportDistance = (height: number, fov: number): number => {
return height / (2 * Math.tan((fov * Math.PI) / 360))
}
بدون وارد شدن به ریاضیات، این محاسبه فاصله ای را که ارتفاع ظاهری دوربین با آن مطابقت دارد، پیدا می کند. height. با مقیاس بندی یک صفحه 1×1 به عرض و ارتفاع دریچه دید، کل مش دقیقاً در میدان دید دوربین (FOV) قرار می گیرد.
کلاس شبکه
در ادامه، بیایید نگاهی کوتاه بیندازیم Mesh کلاس همه چیز مربوط به مش، از جمله اندازه پنجره، بارگذاری بافت، هندسه و مش بندی مواد، اندازه مش، و مدیریت یکنواخت و به روز رسانی را کنترل می کند. شبیه به Stage کلاس، ساختار آن نسبتاً استاندارد است، بنابراین از این به بعد ظاهر نهایی را گام به گام از طریق اجرای سایه بان می سازیم.
سایه زن ورتکس
از آنجایی که این اثر هیچ رئوسی را تغییر نمیدهد، سایهزن رأس ساده است: مختصات UV هندسه را به سایهزن قطعه منتقل میکند و موقعیتهای راس هواپیما را به مختصات فضای صفحه تبدیل میکند که مقیاس مش، موقعیت دوربین، FOV و تنظیمات مربوطه را در بر میگیرد.
precision highp float;
attribute vec3 position;
attribute vec2 uv;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
varying vec2 v_uv;
void main() {
v_uv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
سایه زن قطعه
نمایش بافت ها در تمام صفحه
حالا بیایید به قسمت shader بریم. با نمایش دو تصویر استفاده شده در این افکت به صورت تمام صفحه شروع می کنیم. کد زیر در Mesh کلاس ها setTexture روش آنها را بارگذاری می کند: texture1 تصویر رنگی است که در داخل مربع نمایش داده می شود texture2 تصویر خاکستری است که در ناحیه خارج از آن نشان داده شده است.
const loader = new TextureLoader()
const texture1Path = this.$target.dataset.texture1Path
const texture2Path = this.$target.dataset.texture2Path
if (!texture1Path || !texture2Path) return
const [texture1, texture2] = await Promise.all([
loader.loadAsync(texture1Path),
loader.loadAsync(texture2Path)
])
this.uniforms.u_texture1.value = texture1
this.uniforms.u_texture2.value = texture2
this.uniforms.u_textureSize1.value.set(
texture1.image.width,
texture1.image.height
)
this.uniforms.u_textureSize2.value.set(
texture2.image.width,
texture2.image.height
)
در قطعه سایه زن، رنگهای هر دو تصویر (بافت) را نمونهبرداری میکنیم و ابتدا تأیید میکنیم که هر کدام صفحه را پر میکنند. دو تصویر استفاده شده در اینجا نسبت ابعاد یکسانی دارند، بنابراین u_textureSize1 و u_textureSize2 را می توان در یک مقدار ترکیب کرد. با این حال، ما آنها را به طور جداگانه با استفاده از ابعاد هر بافت محاسبه می کنیم تا از تصاویر با نسبت های مختلف نیز پشتیبانی کنیم.
precision highp float;
uniform sampler2D u_texture1;
uniform sampler2D u_texture2;
uniform vec2 u_meshSize;
uniform vec2 u_textureSize1;
uniform vec2 u_textureSize2;
varying vec2 v_uv;
#include "../chunks/coverUv.glsl"
void main() {
vec2 texture1Uv = getCoverUv(v_uv, u_meshSize, u_textureSize1);
vec2 texture2Uv = getCoverUv(v_uv, u_meshSize, u_textureSize2);
vec4 insideColor = texture2D(u_texture1, texture1Uv);
vec4 outsideColor = texture2D(u_texture2, texture2Uv);
gl_FragColor = insideColor;
}
را getCoverUv عملکرد مورد استفاده در اینجا از کشیده شدن تصویر در زمانی که نسبت تصویر آن با درگاه دید متفاوت است، جلوگیری می کند. به جای استفاده v_uv به طور مستقیم، مختصات UV ایجاد می کند که نسبت تصویر را حفظ می کند و آن را از مرکز برش می دهد، درست مانند CSS. background-size: cover.
vec2 getCoverUv(vec2 uv, vec2 meshSize, vec2 textureSize) {
vec2 meshRatio = vec2(meshSize.x / meshSize.y, meshSize.y / meshSize.x);
vec2 textureRatio = vec2(textureSize.x / textureSize.y, textureSize.y / textureSize.x);
vec2 resolutionRatio = vec2(
min(meshRatio.x / textureRatio.x, 1.0),
min(meshRatio.y / textureRatio.y, 1.0)
);
return (uv - 0.5) * resolutionRatio + 0.5;
}
اکنون تصویر رنگی مطابق شکل زیر صفحه را پر می کند.

می توانید کد را تا این مرحله در 01-display-color-image.glsl پیدا کنید.
هنگامی که تصویر رنگی قابل مشاهده است، آخرین خط را با کد زیر جایگزین کنید و تأیید کنید که تصویر سیاه و سفید نیز نمایش داده می شود.
gl_FragColor = outsideColor;

می توانید کد را تا این مرحله در 02-display-grayscale-image.glsl پیدا کنید.
مقادیر تصویر رنگ و خاکستری نامگذاری شده اند insideColor و outsideColorبه ترتیب، نقش آنها در نتیجه نهایی قابل درک تر شود.
ساختن سیستم مختصات برای مربع
در مرحله بعد، ماسکی ایجاد می کنیم که بین تصاویر رنگی و خاکستری جابجا می شود. برای سهولت کار با مرکز و اندازه مربع، تبدیل می کنیم v_uv به -1.0 to 1.0 محدوده و یک سیستم مختصات با نقطه شروع در مرکز صفحه ایجاد کنید.
vec2 uvSquare = v_uv * 2.0 - 1.0;
یک ماسک مربع ایجاد کنید
استفاده می کنیم uvSquare مختصات ایجاد شده در بالا برای ساخت ماسک. درمان u_squareSize به اندازه نصف طول یک ضلع مربع، مرزهای چپ، راست، پایین و بالای آن را مشخص می کنیم.
uniform float u_squareSize;
float squareHalfSize = u_squareSize;
float left = -squareHalfSize;
float right = squareHalfSize;
float bottom = -squareHalfSize;
float top = squareHalfSize;
استفاده می کنیم step برای بررسی اینکه آیا پیکسل فعلی در هر یک از چهار مرز قرار دارد یا خیر، سپس چهار نتیجه را با هم ضرب کنید. این تولید می کند 1.0 داخل مربع که در آن هر شرط برآورده می شود و 0.0 هر جای دیگر
float squareMask =
step(left, uvSquare.x)
* (1.0 - step(right, uvSquare.x))
* step(bottom, uvSquare.y)
* (1.0 - step(top, uvSquare.y));
برای بررسی شکل ماسک، خروجی می گیریم squareMask ارزش مستقیم به عنوان رنگ
gl_FragColor = vec4(vec3(squareMask), 1.0);
داخل ماسک سفید و بیرون سیاه به نظر می رسد و یک مستطیل سفید در مرکز ایجاد می کند.

می توانید کد را تا این مرحله در 03-create-square-mask.glsl پیدا کنید.
اصلاح نسبت ها
اکنون ما یک ماسک مستطیل شکل داریم، اما نسبت ابعاد آن با تغییر اندازه درگاه نمایش تغییر می کند. برای تصحیح این، از ابعاد شبکه برای محاسبه ضریب تصحیح نسبت ابعاد برای تعیین مرزهای مربع استفاده می کنیم.
vec2 squareAspectScale = vec2(
min(u_meshSize.y / u_meshSize.x, 1.0),
min(u_meshSize.x / u_meshSize.y, 1.0)
);
مختصات مورد استفاده برای تست مربع را بر این ضریب تصحیح نسبت ابعاد تقسیم می کنیم و ناحیه مرئی ماسک را طوری تنظیم می کنیم که مربع به صورت افقی یا عمودی کشیده نشود.
uvSquare /= squareAspectScale;

می توانید کد را تا این مرحله در 04-correct-mask-aspect-ratio.glsl پیدا کنید.
ترکیب دو تصویر
استفاده می کنیم squareMask ارزش برای ترکیب تصاویر رنگی و خاکستری ماسک کجاست 0.0، outsideColor انتخاب شده است؛ جایی که هست 1.0، insideColor انتخاب شده است. نتیجه یک برش مربعی از تصویر رنگی است که در تصویر مقیاس خاکستری نشان داده شده است.
vec4 finalColor = mix(outsideColor, insideColor, squareMask);
gl_FragColor = finalColor;

می توانید کد را تا این مرحله در 05-composite-images.glsl پیدا کنید.
اعوجاج لنز CC را اعمال کنید
سپس اعوجاج سبک لنز CC را روی تصویر رنگی داخل مربع اعمال می کنیم. ابتدا تبدیل می کنیم uvSquare به 0.0 to 1.0 محدوده برای ایجاد مختصات UV محلی به نام squareUv. این مختصات به ما امکان می دهد اعوجاج عدسی را نسبت به مرکز مربع محاسبه کنیم، حتی با تغییر اندازه مربع.
vec2 squareUv = uvSquare / (squareHalfSize * 2.0) + 0.5;
سپس این مختصات UV را به آنها منتقل می کنیم getCCLensUv عملکردی برای ایجاد مختصات UV تحریف شده توسط اثر لنز CC. تابع به صورت زیر تعریف می شود:
uniform float u_lensDistortion;
float getCCLensScale(float distortion, float radius2) {
if (distortion >= 0.0) {
return 1.0 + distortion * radius2;
}
return 1.0 / (1.0 - distortion * radius2);
}
vec2 getCCLensUv(vec2 uv, vec2 resolution, float distortion) {
vec2 centeredUv = uv - 0.5;
vec2 aspectScale = vec2(resolution.x / resolution.y, 1.0);
vec2 centeredPosition = centeredUv * aspectScale;
float radius2 = dot(centeredPosition, centeredPosition);
float lensScale = getCCLensScale(distortion, radius2);
vec2 distortedPosition = centeredPosition * lensScale;
vec2 distortedCenteredUv = distortedPosition / aspectScale;
vec2 distortedUv = distortedCenteredUv + 0.5;
vec2 distortionOffset = distortedUv - uv;
return uv - distortionOffset;
}
تابع ابتدا استفاده می کند uv - 0.5 برای انتقال مرکز مختصات UV به مبدا، سپس استفاده می کند dot برای محاسبه مجذور فاصله از آن مرکز. بر اساس این فاصله، a را محاسبه می کند scale که برای پیکسل ها بیشتر از مرکز تغییر می کند و موقعیت نمونه برداری بافت را تغییر می دهد. از آنجایی که تابع را به مختصات UV محلی داخل یک مربع اعمال می کنیم، عبور می کنیم vec2(1.0) مانند resolution به نمایندگی از a 1:1 نسبت تصویر برای distortion، ما به u_lensDistortion یکنواخت است تا بعداً بتوان مقدار آن را از طریق رابط کاربری گرافیکی تغییر داد.
vec2 distortedSquareUv = getCCLensUv(squareUv, vec2(1.0), u_lensDistortion);
حالا داریم distortedSquareUv، مختصات UV محلی تاب خورده در داخل مربع است. با این حال، استفاده از آنها به طور مستقیم به عنوان مختصات UV تصویر رنگی، کل تصویر را به یک مربع تبدیل می کند. ما میخواهیم که تصاویر خاکستری و رنگی در اندازه و موقعیت در یک راستا باقی بمانند، در حالی که فقط تصویر رنگی را تاب میدهد، بنابراین از تفاوت بین مختصات UV قبل و بعد از تاب برداشتن، یک افست را محاسبه میکنیم. squareLensOffset نشان می دهد که اثر لنز CC چقدر مختصات UV محلی را در مربع جابجا کرده است.
vec2 squareLensOffset = distortedSquareUv - squareUv;
افزودن این افست به طور مستقیم به تمام صفحه v_uv با یک حرکت نسبی مربعی به گونه ای برخورد می کند که گویی نسبت به کل صفحه است و باعث می شود اعوجاج بیش از حد بزرگ شود. بنابراین، آن را در اندازه مجذور و ضریب تصحیح نسبت ابعاد ضرب می کنیم و افست نسبی مجذور را به viewportLensOffset، نسبت به مختصات UV تمام صفحه تغییر می کند.
vec2 viewportLensOffset = squareLensOffset * squareHalfSize * squareAspectScale;
تبدیل شده را اضافه می کنیم viewportLensOffset به v_uv برای ایجاد مختصات UV مورد استفاده برای نمونه برداری از تصویر رنگی.
vec2 lensTexture1Uv = getCoverUv(v_uv + viewportLensOffset, u_meshSize, u_textureSize1);
سپس جایگزین می کنیم texture1Uv قبلا برای نمونه گیری استفاده می شد insideColor با lensTexture1Uv.
vec4 insideColor = texture2D(u_texture1, lensTexture1Uv);
اکنون تصویر رنگی با اعوجاج لنز در داخل ماسک مربع ظاهر می شود.

میتوانید کد را تا این مرحله در 06-apply-cc-lens-distortion.glsl پیدا کنید.
افست RGB شعاعی را اضافه کنید
علاوه بر اعوجاج لنز، یک شیفت RGB را در داخل مربع اعمال می کنیم. به سادگی افزودن یک مقدار ثابت به مختصات UV، رنگ ها را در یک جهت و به همان میزان در همه جای مربع تغییر می دهد. در عوض، مانند افکت لنز، از مرکز مربع به عنوان نقطه مرجع استفاده می کنیم تا تغییر رنگی در مرکز وجود نداشته باشد و تغییر به سمت بیرون قوی تر شود.
را squareUv مختصات مورد استفاده برای تاب برداشتن لنز مختصات UV محلی هستند، جایی که گوشه سمت چپ پایین مربع است 0.0 و گوشه سمت راست بالاست 1.0. برای افست RGB، به جهت و فاصله از مرکز مربع تا هر پیکسل نیاز داریم. ابتدا بیرون می آوریم 0.5 از squareUv برای انتقال مرکز مربع به مبدا، سپس ضرب در 2.0 برای ایجاد rgbShiftDirection مستقل از اندازه -1.0 to 1.0 محدوده
vec2 rgbShiftDirection = (squareUv - 0.5) * 2.0;
اندازه شیفت برای هر کانال RGB از طریق سه لباس ارائه می شود: u_rgbShiftR، u_rgbShiftG، و u_rgbShiftB. این مقادیر از طریق رابط کاربری گرافیکی نیز قابل تنظیم خواهند بود. هر جزء از rgbShiftDirection است 0.0 در مرکز میدان و می رسد -1.0 یا 1.0 در لبه مربوطه، بنابراین هر یکنواخت حداکثر افست UV را در امتداد محور آن لبه تنظیم می کند. مقادیر مثبت و منفی در جهت مخالف تغییر می کنند، در حالی که 0.0 این کانال را بدون تغییر می گذارد.
uniform float u_rgbShiftR;
uniform float u_rgbShiftG;
uniform float u_rgbShiftB;
سپس، با شروع از لنز تحریف شده است lensTexture1Uv، برای هر کانال یک افست با ضرب اضافه می کنیم rgbShiftDirection با مقدار شیفت های او سپس مقادیر انتخابی R، G و B را دوباره با هم ترکیب می کنیم insideColor.
float r = texture2D(u_texture1, lensTexture1Uv + rgbShiftDirection * u_rgbShiftR).r;
float g = texture2D(u_texture1, lensTexture1Uv + rgbShiftDirection * u_rgbShiftG).g;
float b = texture2D(u_texture1, lensTexture1Uv + rgbShiftDirection * u_rgbShiftB).b;
vec4 insideColor = vec4(r, g, b, 1.0);
افست RGB قبلاً اعمال شده است. با مقادیر پیش فرض 0.01 برای R، 0.0 برای G و -0.01 برای B، هر سه کانال در مرکز مربع همپوشانی دارند، در حالی که R و B در جهت مخالف به سمت لبه ها واگرا می شوند. بعداً که بیرون می آوریم u_mouse از uvSquare برای جابجایی مربع، مبدا تغییر RGB به دست آمده از squareUv با مرکز عدسی حرکت می کند.

می توانید کد را تا این مرحله در 07-apply-rgb-shift.glsl پیدا کنید.
مربع را به دنبال ماوس قرار دهید
سپس ماسک مربع را درست می کنیم و مرکز لنز به دنبال ماوس می آید. a اضافه کنید u_mouse یکنواخت و به ترتیب زیر:
uniform vec2 u_mouse;
uvSquare -= u_mouse;
مختصات ماوس DOM بر حسب پیکسل از گوشه بالا سمت چپ اندازه گیری می شود، در حالی که uvSquare از مختصات در استفاده می کند -1.0 to 1.0 محدوده ای که از مرکز صفحه شروع می شود. بنابراین، مختصات ماوس را در TypeScript به همان محدوده تبدیل می کنیم.
public onPointerMove(event: PointerEvent): void {
this.mouse.set(
(event.clientX / this.windowWidth) * 2 - 1,
-(event.clientY / this.windowHeight) * 2 + 1
)
}
مختصات Y نفی می شود زیرا مختصات DOM به سمت پایین افزایش می یابد، در حالی که سایه زن به سمت بالا به عنوان یک جهت مثبت برخورد می کند.
سپس، در داخل render روشی که ما استفاده می کنیم Vector2.lerp برای درونیابی خطی و نتیجه را کپی کنید this.mouseEase ارزش به u_mouse، با جلوه ای که به آرامی ماوس را دنبال می کند.
this.mouseEase.lerp(this.mouse, this.pointerEase)
this.uniforms.u_mouse.value.copy(this.mouseEase)
ماسک مربعی و مرکز لنز اکنون از موقعیت ماوس به عنوان نقطه شروع استفاده می کنند و حرکت آن را به آرامی دنبال می کنند.
می توانید کد را تا این مرحله در 08-follow-mouse.glsl پیدا کنید.
حرکت را به تصویر خارجی اضافه کنید
در مرحله بعد، فقط به تصویر سیاه و سفیدی که صفحه را می پوشاند، حرکت ظریف اضافه می کنیم. از آنجایی که هم تاب موج و هم تاب تصادفی که می خواهیم در طول زمان متحرک ایجاد کنیم، ابتدا اضافه می کنیم u_time یکنواخت
uniform float u_time;
در سمت TypeScript، زمان سپری شده را تبدیل می کنیم performance.now() به ثانیه و آن را منتقل کنید u_time روی هر قاب
this.uniforms.u_time.value = performance.now() * 0.001
یک اعوجاج موج اضافه کنید
استفاده می کنیم sin برای افزودن افکت موج دار سه مقدار u_waveFrequency، u_waveSpeed، و u_waveStrength به عنوان یونیفرم اضافه می شوند تا بتوان آنها را از طریق رابط کاربری گرافیکی تنظیم کرد.
uniform float u_waveFrequency;
uniform float u_waveSpeed;
uniform float u_waveStrength;
با استفاده از این یونیفرم ها، یک موجک افست را در موقعیت نمونه برداری تصویر در مقیاس خاکستری در امتداد محور Y اعمال می کنیم.
float wave = sin(texture2Uv.y * u_waveFrequency + u_time * u_waveSpeed) * u_waveStrength;
texture2Uv.y += wave;
اعوجاج تصادفی را اضافه کنید
سپس استفاده می کنیم random3 عملکردی برای اضافه کردن اعوجاج های ظریف و نویز مانند. مانند موج، u_randomFrequency، u_randomSpeed، و u_randomStrength به عنوان یونیفرم اضافه می شوند تا بتوان آنها را از طریق رابط کاربری گرافیکی تنظیم کرد.
uniform float u_randomFrequency;
uniform float u_randomSpeed;
uniform float u_randomStrength;
را random3 از این مثال شادرتوی اقتباس شده است. به جای اینکه این تابع را مستقیماً در قسمت shader بنویسیم، آن را به قسمت منتقل می کنیم glsl/chunks/random3.glsl به طوری که می تواند توسط سایه بان های دیگر مورد استفاده مجدد قرار گیرد.
//
// by Nikita Miropolskiy
vec3 random3(vec3 c) {
float j = 4096.0 * sin(dot(c, vec3(17.0, 59.4, 15.0)));
vec3 r;
r.z = fract(512.0 * j);
j *= 0.125;
r.x = fract(512.0 * j);
j *= 0.125;
r.y = fract(512.0 * j);
return r - 0.5;
}
قبل از بله main تابع ج frag.glsl، این قسمت را به همان صورت درج می کنیم coverUv.glsl و ccLens.glsl. در حین ساخت، بدنه تابع در داخل قرار می گیرد #include بخشنامه اجازه می دهد main تا آن را به عنوان یک تابع GLSL معمولی صدا کنید.
#include "../chunks/random3.glsl"
داخل main، استفاده می کنیم random3 تابع جبران texture2Uv به شرح زیر:
texture2Uv += random3(vec3(texture2Uv * u_randomFrequency, u_time * u_randomSpeed)).x * u_randomStrength;
این اثر نشان داده شده در ابتدا را کامل می کند!

اضافه کردن کنترل های رابط کاربری گرافیکی
جلوه بصری و تعامل اکنون کامل شده است. در نهایت، پارامترها را به رابط کاربری گرافیکی اضافه می کنیم تا بتوانیم اندازه مربع، قدرت لنز، مقادیر افست RGB را تنظیم کنیم. wave و random حرکت و غیره
پارامترهای سایه زن را تعریف کنید
را Webgl کلاس منطقی را مدیریت می کند که دمو را با هم پیوند می دهد، از جمله مقداردهی اولیه Stage و Mesh کلاس ها، ثبت رویداد و حلقه رندر. از آنجایی که این یک ساختار معمولی است، از جزئیات صرف نظر می کنیم و روی ایجاد رابط کاربری گرافیکی و به روز رسانی پارامترهای سایه زن تمرکز می کنیم. مقادیر پارامترهای پیش فرض در مدیریت می شوند Webgl کلاس ها constructor.
this.shaderParams = {
squareSize: 0.3,
lensDistortion: 1.5,
rgbShiftR: 0.01,
rgbShiftG: 0,
rgbShiftB: -0.01,
waveFrequency: 10,
waveStrength: 0.01,
waveSpeed: 1,
randomFrequency: 1,
randomStrength: 0.02,
randomSpeed: 0.2,
pointerEase: 0.1
}
ایجاد رابط کاربری گرافیکی
رابط کاربری گرافیکی ایجاد می شود و پارامترهای آن در آن اضافه می شود Webgl کلاس ها setGUI روش در اینجا ما پارامترها را به پوشه های سازماندهی شده توسط افکت اضافه می کنیم.
private setGUI(): void {
this.destroyGUI()
this.gui = new GUI({
title: "Square Texture Effect"
})
this.gui.onChange(this.updateShaderParams)
const squareFolder = this.gui.addFolder("Square")
const rgbShiftFolder = this.gui.addFolder("RGB Shift")
const waveFolder = this.gui.addFolder("Wave")
const randomFolder = this.gui.addFolder("Random")
const pointerFolder = this.gui.addFolder("Pointer")
squareFolder
.add(this.shaderParams, "squareSize", 0, 5, 0.01)
.name("Square Size")
squareFolder
.add(this.shaderParams, "lensDistortion", -5, 5, 0.01)
.name("Lens Distortion")
rgbShiftFolder
.add(this.shaderParams, "rgbShiftR", -0.05, 0.05, 0.001)
.name("Red Shift")
rgbShiftFolder
.add(this.shaderParams, "rgbShiftG", -0.05, 0.05, 0.001)
.name("Green Shift")
rgbShiftFolder
.add(this.shaderParams, "rgbShiftB", -0.05, 0.05, 0.001)
.name("Blue Shift")
waveFolder
.add(this.shaderParams, "waveFrequency", 0, 200, 1)
.name("Wave Frequency")
waveFolder
.add(this.shaderParams, "waveStrength", 0, 0.1, 0.001)
.name("Wave Strength")
waveFolder
.add(this.shaderParams, "waveSpeed", 0, 5, 0.01)
.name("Wave Speed")
randomFolder
.add(this.shaderParams, "randomFrequency", 0.1, 20, 0.1)
.name("Random Frequency")
randomFolder
.add(this.shaderParams, "randomStrength", 0, 0.1, 0.001)
.name("Random Strength")
randomFolder
.add(this.shaderParams, "randomSpeed", 0, 2, 0.01)
.name("Random Speed")
pointerFolder
.add(this.shaderParams, "pointerEase", 0.01, 1, 0.01)
.name("Pointer Ease")
}
لباس های فرم را به روز کنید
هنگامی که یک پارامتر در رابط کاربری گرافیکی تغییر می کند، shaderParams منتقل می شود به Mesh کلاس از طریق updateShaderParams روش
private updateShaderParams = (): void => {
if (this.mesh) {
this.mesh.setShaderParams(this.shaderParams)
}
}
سی Mesh کلاس، مقادیر دریافت شده از رابط کاربری گرافیکی مطابق شکل زیر به یونیفرم های مربوطه اختصاص داده می شود. چون pointerEase به سایه زن منتقل نمی شود، به عنوان یک ویژگی از به روز می شود Mesh در عوض کلاس این به ما امکان می دهد مقادیر پارامترها را در زمان واقعی تغییر دهیم.
public setShaderParams({
squareSize,
lensDistortion,
rgbShiftR,
rgbShiftG,
rgbShiftB,
waveFrequency,
waveStrength,
waveSpeed,
randomFrequency,
randomStrength,
randomSpeed,
pointerEase
}: ShaderParams): void {
this.uniforms.u_squareSize.value = squareSize
this.uniforms.u_lensDistortion.value = lensDistortion
this.uniforms.u_rgbShiftR.value = rgbShiftR
this.uniforms.u_rgbShiftG.value = rgbShiftG
this.uniforms.u_rgbShiftB.value = rgbShiftB
this.uniforms.u_waveFrequency.value = waveFrequency
this.uniforms.u_waveStrength.value = waveStrength
this.uniforms.u_waveSpeed.value = waveSpeed
this.uniforms.u_randomFrequency.value = randomFrequency
this.uniforms.u_randomStrength.value = randomStrength
this.uniforms.u_randomSpeed.value = randomSpeed
this.pointerEase = pointerEase
}
رابط کاربری گرافیکی نیز اکنون کامل شده است. سعی کنید مقادیر را تنظیم کنید تا ببینید چگونه ظاهر افکت را تغییر می دهند!
نتیجه گیری
چه فکر کردید امیدوارم این مقاله نشان داده باشد که حتی جلوه ای که در نگاه اول پیچیده به نظر می رسد را می توان با ترکیب عناصر کوچک و ساده ساخت. شما همچنین می توانید پارامترهای خود را به این پیاده سازی اضافه کنید یا محدوده های قابل تنظیم را تغییر دهید تا چیزی اصلی ایجاد کنید. این پروژه از تصویری الهام گرفته شده است که من در Pinterest پیدا کردم، اما ایده ها در همه جا وجود دارند: در تصاویر، وب سایت ها، ویدیوها و زندگی روزمره. وقتی چیزی توجه شما را جلب کرد، سعی کنید آن را به کد تبدیل کنید، مانند آنچه در اینجا انجام دادیم. در نهایت، اگر در مورد این مقاله سؤالی دارید، با من در X تماس بگیرید. از خواندن شما متشکرم!



