ساخت یک شبکه شیشه ای مایع بی نهایت با Three.js، WebGPU و TSL

یادداشت سردبیر: با توجه به اینکه کارگاه های کنفرانس Three.js در حال انجام است و کنفرانس تنها دو روز باقی مانده است، به نظر می رسد بهترین زمان برای رفتن به پشت صحنه آزمایشی است که بسیار سر و صدا کرده است. Shader’s Jacob، Simon و Philip این مش بی نهایت مسحورکننده از شیشه مایع را به عنوان یک کاوش بازیگوش در Three.js، WebGPU و TSL ایجاد کردند. اکنون آنها پرده را عقب می کشند تا به ما نشان دهند که چگونه آن را ساخته اند، از شیشه های تقلبی و شکستگی تا شبکه های کروی بی نهایت. ما هیجان زده هستیم که آنها داستان پشت آزمایش را به عنوان بخشی از جشن کنفرانس ما به اشتراک بگذارند.
🇫🇷 دو روز مونده! اولین کنفرانس Three.js تقریباً اینجا در پاریس است. از یک کد استفاده کنید CODROPS برای 15 درصد تخفیف و بلیط خود را دریافت کنید →
هی من فیلیپ هستم، یکی از بنیانگذاران Shader، یک استودیوی توسعه خلاق در سوئد. در بیشتر ایام سال، تجربیات سه بعدی تعاملی را برای مشتریان ارائه می کنیم. چند هفته از آن را صرف انجام آزمایش های عجیب و غریب بدون ضرب الاجل کوتاه و بدون مهلت می کنیم. آخرین مورد، Liquid Glass Infinite Carousel است: شبکهای از کارتهای ویدئویی شیشهای که در مرورگر WebGPU اجرا میشوند. ویدئو از طریق شیشه خم می شود، شبکه هیچ لبه ای ندارد و حتی یک نور در صحنه وجود ندارد.
پشته
Next.js با React Three Fiber v10 در رندر WebGPU. مواد به زبان TSL (زبان سایه دهی Three.js) نوشته شده اند، Motion فنرهای درگ را کنترل می کند و جریان های ویدیویی HLS به عنوان بافت استفاده می شوند. بدون پس پردازش و بدون اهداف رندر. همه چیز در یک گذر اتفاق می افتد.
شیشه مایع، بدون نیاز به هندسه
ترفند در اینجا این است که هر نقشه فقط یک صفحه مسطح و تقسیم شده است. گوشه های گرد، لبه های اریب و پیچ خوردگی در مواد نادرست هستند. ما هرگز شیشه را مدل نمی کنیم.
اگر از Three.js استفاده کرده اید، ممکن است فکر کنید که همین است MeshPhysicalMaterial برای است. و بس: یک مجموعه transmission، ior، thickness، و dispersion روی یک کیس گرد و شیشه بسیار قانع کننده ای از قاب دریافت می کنید. اما این شیشه هندسه واقعی است، به یک صحنه روشن نیاز دارد و رندر ابتدا همه چیز پشت جسم را در یک بافر جداگانه رندر می کند. این ماشین برای چند صد کارت است و صحنه پشت کارت را می شکند وقتی چیزی که ما واقعاً می خواهیم خم کنیم ویدیوی خود کارت است.
بنابراین ما خودمان را رول می کنیم: یک صفحه تخت، یک سایه زن سفارشی کوچک و یک رندر.
همه چیز با یک تابع فاصله دو بعدی شروع می شود. برای هر پیکسل روی نقشه، فاصله تا لبه یک مستطیل گرد را به ما می دهد. منفی یعنی درون، مثبت یعنی بیرون. این جعبه گرد کلاسیک SDF است که در TSL نوشته شده است:
const roundedBoxDistance = Fn(([point]) => {
const radius = min(u.cornerRadius, min(halfSize.x, halfSize.y));
const q = abs(point).sub(halfSize).add(radius);
return length(max(q, vec2(0)))
.add(min(max(q.x, q.y), float(0)))
.sub(radius);
});

از این فاصله یک نقشه ارتفاع می سازیم. آن را به عنوان ضخامت شیشه در هر نقطه در نظر بگیرید: صاف در وسط، سپس به آرامی در لبهها به سمت صفر خمیده میشود. منحنی یک ابر بیضی است، بنابراین bevelPower به ما اجازه می دهد از یک بالشتک نرم به یک پخ تیز و عظیم برسیم:
const bevelHeight = Fn(([distance]) => {
const edge = clamp(
float(1).add(distance.div(max(uniforms.bevelWidth, float(0.001)))),
0,
1,
);
const power = max(uniforms.bevelPower, float(1));
const profile = pow(
max(float(1).sub(pow(edge, power)), float(0)),
float(1).div(power),
);
return profile.mul(uniforms.thickness);
});
برای خم شدن شیشه، باید بدانیم سطح در هر پیکسل به کدام سمت است. بنابراین از نقشه ارتفاع با یک گام کوچک به چپ، راست، بالا و پایین مثال می زنیم. تفاوت بین این نمونه ها به ما شیب می دهد و شیب به ما هنجار می دهد. این همان ترفندی است که برای محاسبه نرمال ها از نقشه ارتفاع زمین که به سادگی روی شیشه های تقلبی ما اعمال می شود، استفاده می کنید.

حالا برای بخش خوب. هنگامی که یک جهت عادی و دید داشته باشیم، می توانیم پرتو را شکست دهیم. اگر آن را چندین بار با ضریب شکست کمی متفاوت برای هر کانال رنگی شکست دهیم، پراکندگی رنگی به دست می آید، نوارهای رنگین کمانی که در اطراف لبه های شیشه واقعی می بینید. هر “لمس” پرتو را کمی متفاوت خم می کند و عمدتاً به یک کانال رنگی وارد می شود:
let refracted = vec3(0);
for (const tap of taps) {
const eta = float(1).div(
max(uniforms.ior.add(uniforms.dispersion.mul(tap.offset)), float(1.0001)),
);
const ray = refract(viewDir.negate(), normal, eta);
const travel = uniforms.thickness.div(max(abs(ray.z), float(0.05)));
const displaced = baseUv.add(
ray.xy.mul(travel).mul(uniforms.refractStrength).div(uniforms.planeSize),
);
refracted = refracted.add(
mapNode.sample(displaced).rgb.mul(vec3(...tap.weight)),
);
}
توجه داشته باشید که این جاوا اسکریپت ساده است for چرخه TSL سایهزن را بهعنوان یک گراف گره میسازد، بنابراین زمانی که شیدر کامپایل میشود، حلقه تکامل مییابد. این باعث می شود شمارنده لمسی فقط یک دکمه کیفیت باشد: بیشتر روی GPU های قوی لمس می شود، در دستگاه های تلفن همراه کمتر، در حالی که کد سایه زن دقیقاً یکسان می ماند.
بافتی که شکسته می شود ویدئوی خود کارت است، بنابراین ویدئو به معنای واقعی کلمه از طریق شیشه خم می شود. این لایه پایه است. برای اینکه آن را بهعنوان ویدیوی شیشهای و بدون لرزش بخوانید، دو ماده دیگر را در بالا اضافه میکنیم، و هیچ نوری در صحنه برای هیچکدام از آنها وجود ندارد.
اول، بازتاب. جهت نمای اطراف سطح را به صورت عادی منعکس می کنیم و از این جهت برگشتی برای جستجوی رنگ در نقشه محیطی پیچیده شده در اطراف یک کره استفاده می کنیم. این باعث میشود که هر قسمت از لیوان چه چیزی را منعکس میکند، یک جستجوی ساده برای بافت پیدا کنید. درجه بازتابی که ما نشان میدهیم توسط فرنل کنترل میشود: وقتی مستقیم به آن نگاه میکنید، شیشه بسیار کمی منعکس میشود و در زاویه شیب بسیار بیشتر.
دوم، رینگ لایت. الف smoothstep در SDF فاصله یک نوار نازک به ما می دهد که لبه نقشه را در آغوش می گیرد و ما آن را با یک گرادیان از بالا به پایین بین دو رنگ رنگ می کنیم تا برجسته به نظر برسد که جهت دارد.
با هم جمع کنید:
const reflection = reflect(viewWorld.negate(), normalWorld);
const environment = texture(envMap, equirectUV(reflection)).rgb;
const fresnel = u.fresnelF0.add(
float(1)
.sub(u.fresnelF0)
.mul(pow(saturate(float(1).sub(dot(normal, viewDir))), 5)),
);
const rim = smoothstep(u.rimWidth.negate(), float(0), distance).mul(u.rimIntensity);
const rimColor = mix(u.rimColor, u.rimColorTop, rimGradient);
const finished = mix(
refracted.mul(u.tint),
environment,
saturate(fresnel.mul(u.envIntensity)),
).add(rimColor.mul(rim));
این همه مواد است. این یک است MeshBasicNodeMaterial با سفارشی colorNodeو هر بخشی از “روشنایی” که می بینید، از حالت عادی که خودمان محاسبه کرده ایم جعلی است.

وب بی نهایتی که مخفیانه روی یک کره زندگی می کند
ترفند اینجاست: وب اصلا بی نهایت نیست. به اندازه کافی آن را بکشید و کارتی که یک انتها را ترک میکند بیصدا به طرف دیگر تلهپورت میشود.
با این حال، شبکه بسته بندی مسطح مانند یک صفحه گسترده به نظر می رسد. بنابراین بهجای گذاشتن کارتها در هواپیما، با آنها رفتار میکنیم x و y موقعیت ها را به عنوان مسافت طی شده روی یک کره غول پیکر قرار دهید و هر کارت را روی سطح آن رو به پایین قرار دهید. کارتها در مرکز رو به شما هستند، در حالی که هرچه به سمت بیرون میروند، بیشتر به سمت منحنی کج میشوند. مغز شما این را بهعنوان عمق بهجای یک صفحه تخت بیانتها میخواند.
کشیدن خود توسط حرکت حرکت پان در یک عنصر تمام صفحه انجام می شود، با موقعیت و سرعت ذخیره شده به عنوان motionValueخارج از React است. چرخهساز فریم R3F آنها را مستقیماً میخواند، بنابراین هنگام کشیدن، رندر مجددی وجود ندارد.
متن واقعی روی شیشه تقلبی
نکته دیگری که ممکن است در ابتدا متوجه آن نشوید: عنوان، دسته و توضیحات هر کارت HTML واقعی است. نه بافت، نه متن SDF، ساده divس این به معنای متن واضح در هر بزرگنمایی، CSS معمولی، محتوای قابل انتخاب و در دسترس است. گرفتار این است که هر کدام div باید دقیقاً روی نقشهای قرار بگیرد که روی کرهای در WebGPU زندگی میکند و همانطور که میکشید آن را کاملاً دنبال میکند.
ترفند همان Three.js است CSS3DRenderer استفاده های ساخته شده با دست لایه تمام صفحه ثابت دریافت CSS perspective به دست آمده از میدان دید دوربین داخل آن یک “دوربین” است div ماتریس دوربین معکوس را به صورت a حمل می کند matrix3d. در داخل آن، یکی کاملاً قرار دارد div در یک نقشه یکپارچه، ماتریس شبکه جهانی را به عنوان خود حمل می کند matrix3d. ریاضیات پرسپکتیو مرورگر همان ریاضیاتی است که GPU از آن استفاده می کند، بنابراین div یک پیکسل ایدهآل را روی شبکه قرار میدهد و با آن از کره عبور میکند:
rootLayer.style.perspective = `${fov}px`;
cameraLayer.style.transform = `translateZ(${fov}px) ${cssMatrix(camera.matrixWorldInverse)} translate(50vw, 50vh)`;
cardEl.style.transform = `translate(-50%,-50%) ${cssMatrix(mesh.matrixWorld)}`;
هر فریم حلقه R3F یک بار قسمت دوربین را بهروزرسانی میکند، سپس در استخر حلقه میزند و در هر نقشه قابل مشاهده، تبدیلی به div مینویسد (و نقشههای خارج از صفحه را پنهان میکند). ما مقیاس را از ماتریس حذف می کنیم و در عوض آن را به صورت عرض و ارتفاع بر حسب پیکسل اعمال می کنیم، که به خود متن اجازه می دهد تا با واحدهای ظرف مقیاس شود و واضح بماند. همان قانون کشیدن: div ها ادغام می شوند تا با تعداد شبکه مطابقت داشته باشند و با ورودی های style.transform مستقیم به روز می شوند، هرگز از طریق رندر مجدد React.
به روز رسانی: ساختن آن در واقع مایع
پس از ارسال نسخه ی نمایشی، شخصی به این نکته اشاره کرد که تمام شیشه ها “شیشه مایع” نیستند و ما با تبلیغات اپل مواجه شده ایم. این یک نفر بود، اما آنها یک نکته داشتند. بنابراین ما شبکه را واقعاً حرکت دادیم.
اکنون کل سطح یک شبیه سازی فابریک GPU است: یک حل کننده XPBD که به صورت شکاف محاسباتی TSL نوشته شده و کاملاً روی WebGPU اجرا می شود. هر کارت روی یک تکه پارچه شبیهسازی شده با محدودیتهای فاصله و خمش، لنگرهایی که آن را به جای خود روی کره میکشد، و یک اصطلاح ویسکوزیته که باعث میشود آن را بیشتر شبیه ژلهای تاب بخورد، قرار میگیرد تا پارچه. شبکه را بکشید و عقب می افتد، موج می زند و می نشیند. برای یک شبکه پروژه کاملاً اضافی و ساختن بسیار سرگرم کننده است.
لایه متنی DOM از بخش قبلی از این حرکت جان سالم به در نبرد. متن چسبانده شده بر روی یک شبکه سفت یک چیز است، متنی که به دنبال یک پارچه لرزان است چیز دیگری است. برچسبها اکنون در داخل صحنه pmndrs/glyph با استفاده از فونتهای MSDF که در زمان ساخت آماده شدهاند، رندر میشوند، بنابراین همراه با شیشهای که روی آن مینشینند، تاب میخورند و میشکنند.
این همه چیز است: یک SDF که وانمود می کند شیشه است و یک کره که وانمود می کند یک هواپیمای بی نهایت است.
بیا به پاریس سلام کن
من و جیکوب در کنفرانس Three.js در پاریس خواهیم بود. اگر می روی، بیا ما را پیدا کن و سلام کن! ما همیشه آماده گپ زدن در مورد WebGPU، TSL، یا هر نسخه نمایشی عجیب و غریبی هستیم که شما انجام می دهید. برای اینکه راحتتر ما را تشخیص دهید، در اینجا یک عکس بسیار حرفهای از ما با سیمون در سمت راست وجود دارد. او برای نواختن ویولن خود در سوئد می ماند.

و اگر میخواهید کارهای بیشتری را ببینید، صفحه فرود ما در shader.se به خودی خود یک آزمایش کامل است.
آنجا می بینمت!




