راهنمای TSL و WebGPU

اکنون در آخرین نسخه iOS و سافاری اپل پشتیبانی می شود 1،
WebGPU بالاخره پشتیبانی گستردهای را دریافت میکند و قابلیتهای سه بعدی پیشرفتهتری را در وب فراهم میکند.
بهعنوان کسی که با WebGL کناری کار میکند، این فشار لازم بود تا در نهایت مدتی را به یادگیری بیشتر در مورد این مجموعه جدید از APIها اختصاص دهم.
و ببینید چگونه می توانم برخی از دانش موجود خود را در مورد WebGL/shader ها دریافت کنم و با آنها کار کنم.
این کار نه تنها شامل آشنایی با WGSL، زبان سایه زن طراحی شده برای WebGPU، اما همچنین TSL: سطح بالا Three.js،
یک زبان سایه مانند جاوا اسکریپت. علاوه بر این، WebGPU برخی از ساختارهای جدید مانند شیدرهای محاسباتی، یک نوع جدید، هیجان انگیز اما مبهم از شیدرها که به نظر می رسید
می تواند برخی از جنبه های کلیدی گردش کار من را بهبود بخشد.
برای اینکه من از این سفر یادگیری عبور کنم، فکر کردم عملی ترین راه این است که تابستان خود را برای انتقال پروژه های کلیدی که در طول سال ها روی آنها کار کرده ام، مانند مواد شیشه ای، پس پردازش، ذرات، و همچنین چند آزمایش سرگرم کننده سایه زن، سپری کنم.
بسیاری از مسائل و ویژگی های غیر مستند وجود داشت که گاهی اوقات تجربه را کاملا خسته کننده می کرد، اما در نهایت توانستم خودم را با این ابزارهای جدید آشنا کنم و صحنه های زیبایی را با آنها خلق کنم.
این مقاله تلفیقی از مراحلی است که من از دانش بسیار اندک از TSL و WebGPU به راحتی با آنها برداشتم و در نتیجه به راهنمای میدانی که ای کاش داشتم. من آن را در آن انجام خواهم داد تا همه چیز را در مورد این ابزارها/APIهای جدید و نقشی که ایفا می کنند به شما بگویم در حین اشتراک گذاری دستور العمل ها برای پیاده سازی برخی از اجزای ضروری یک صحنه سه بعدی، خواه ساخت یک ماده ساده با استفاده از آن سیستم گره ای جدید TSL، یا برای اهرم شیدرهای محاسباتی در موارد استفاده مختلف مانند مش های نمونه سازی شده، ذرات یا اثرات پس از پردازش.
اگر مانند من، نامهای TSL یا WebGPU را دیدهاید که به جای یکدیگر استفاده میشوند، ممکن است در مورد نقش آنها و نحوه ارتباط آنها با یکدیگر گیج شوید. این بخش اول باید به تمام سوالات شما در مورد این دو مفهوم پاسخ دهد تا بتوانید با یک پایه محکم شروع به ساختن خود کنید.
TSL و WebGPU
بسیاری از توسعه دهندگان خلاق از گسترش اخیر پشتیبانی WebGPU بسیار هیجان زده بودند و دلیل خوبی هم داشت: این مجموعه جدید از APIها، وب را با APIهای گرافیکی مدرن مانند Vulkan یا Metal، که ستون فقرات بسیاری از نرم افزارهای خلاق حرفه ای و موتورهای بازی هستند، تراز می کند. کنترل های سطح پایین (تخصیص حافظه، گروه های اتصال و غیره) را فراهم می کند و ساختارهای جدیدی مانند شیدرهای محاسباتی، در حالی که عملکرد تقریباً بومی دارد 2.
از سوی دیگر، توسعه دهندگان Three.js من نیز به چیزی کمی متفاوت، اما مرتبط با هم علاقه مند بودند: زبان سایه زنی Three.js. عملکردی زبان بر اساس جاوا اسکریپت، که به عنوان چیزی شبیه به آن طراحی شده است یک چتر زبان سایه زنی که ایجاد سایه بان را در طول زمان در دسترس تر و قابل نگهداری تر می کند. با معرفی TSL، Three.js توانایی نوشتن سایهبانهایی را فراهم کرد که میتوانستند روی WebGPU اجرا شوند، که تا آن زمان پشتیبانی نمیشد، در حالی که به همان شیدرها اجازه میداد روی WebGL اجرا شوند، بنابراین به توسعهدهندگان اجازه میداد تا هر پلتفرم را با کد منحصر به فرد بدون توجه به پشتیبانی WebGPU مورد هدف قرار دهند. 3
در زیر نمونه ای از یک سایه زن ساده نوشته شده در TSL و معادل های GLSL و WGSL آن آورده شده است:
1
const colorNode = Fn(([baseColor]) => {4
const red = uvCoord.x.add(2.3).mul(0.3);5
const green = uvCoord.y.add(1.7).div(8.2);6
const blue = add(uvCoord.x, uvCoord.y).mod(10.0);8
const tint = vec4(red, green, blue, 1.0);10
return mix(baseColor, tint, uvCoord.x);
4
float red = (uv.x + 2.3) * 0.3;5
float green = (uv.y + 1.7) / 8.2;6
float blue = mod(uv.x + uv.y, 10.0);8
vec4 tint = vec4(red, green, blue, 1.0);9
fragColor = mix(baseColor, tint, uv.x);
1
fn main(uv: vec2f32>, baseColor: vec4f32>) -> vec4f32> {2
let red = (uv.x + 2.3) * 0.3;3
let green = (uv.y + 1.7) / 8.2;4
let blue = (uv.x + uv.y) % 10.0;6
let tint = vec4f32>(red, green, blue, 1.0);7
return mix(baseColor, tint, uv.x);
همانطور که می بینید، سینتکس حتی برای عملگرهای ساده بسیار کاربردی است (به عبارتی “بسیار”). در حالی که این توانایی هدف قرار دادن WebGL یا WebGPU را به طور مساوی ارائه می دهد، برای من با قربانی شدن خوانایی همراه است. فک کنم سلیقه و عادت باشه. اگر این خیلی شما را آزار می دهد، باز هم می توانید
برای انتخاب کنید glslFn و wgslFn ویژگی TSL برای نوشتن کد خود، اما همچنان باید در پایان روز برای تعریف و مصرف این سایهزنها در مواد/افکتهای خود به TSL تکیه کنید. سیستم گره ای.
از glslFn و wgslFn برای نوشتن کد سایه زن خود به همراه TSL استفاده کنید
1
const baseColor = uniform(new THREE.Vector4(1, 1, 1, 1));3
const colorGLSL = glslFn( `4
vec4 colorFn(vec4 baseColor, vec2 uv) {5
float red = (uv.x + 2.3) * 0.3;6
float green = (uv.y + 1.7) / 8.2;7
float blue = mod(uv.x + uv.y, 10.0);8
return mix(baseColor, vec4(red, green, blue, 1.0), uv.x);12
const colorNodeGLSL = colorGLSL({17
const colorWGSL = wgslFn( `18
fn colorFn(baseColor: vec4f, uv: vec2f) -> vec4f {19
let red = (uv.x + 2.3) * 0.3;20
let green = (uv.y + 1.7) / 8.2;21
let blue = (uv.x + uv.y) % 10.0;22
return mix(baseColor, vec4f(red, green, blue, 1.0), uv.x);26
const colorNodeWGSL = colorWGSL({
بنابراین به طور خلاصه:
-
می توانید از TSL برای نوشتن سایه بان برای WebGL و WebGPU استفاده کنید.
-
اگر بخواهید همچنان می توانید از GLSL و RawShaderMaterial/ShaderMaterial استفاده کنید، اما همیشه محدود به WebGL خواهید بود.
-
شما نمی تواند سایه زن های WebGPU خام بنویسد به همان روشی که در WebGL می توانید: همه چیز از TSL انتزاع شده است.
سیستم گره
اگرچه لایه انتزاعی و عدم توانایی در نوشتن سایهبانهای خام WebGPU برای من کمی بد بود زیرا ترجیح میدهم از ساختهای سطح پایین استفاده کنم. سیستم گره ای TSL برای من این یک ویژگی کلیدی است که مرا فروخت و باعث شد آن را در آغوش بگیرم.
تا به حال، زمانی که میخواستیم مطالب موجود در WebGL را حتی برای کوچکترین تغییرات اصلاح کنیم، مجبور بودیم به سختی از طریق الحاق رشتههای چندین بخش سایهزن با استفاده از آن کار کنیم. onBeforeCompile روش
امروزه TSL برای هر یک از خود قلاب هایی ارائه می دهد NodeMaterials، مانند colorNode، normalNode، یا positionNode، برای تغییر رنگ، نرمال ها و موقعیت راس مواد خود به راحتی بر این اساس. حتی بهتر از آن، هر کالای Three.js اکنون یک «معادل گره» سازگار با این API جدید دارد:
-
MeshStandardMaterialوجود داردMeshStandardNodeMaterial -
MeshBasicMaterialوجود داردMeshBasicNodeMaterial -
MeshPhysicalMaterialوجود داردMeshPhysicalNodeMaterial
و غیره
البته، گرههای دیگری نیز وجود دارند که من در اینجا فهرست نکردهام، و با گسترش مجموعه ویژگیهای TSL، گرههای دیگر نیز وجود دارند. مواردی که ذکر کردم مهمترین و اولین مواردی هستند که هنگام بازی با TSL و شیدرها با آنها مواجه خواهید شد.
نوشتن سایه زن برای WebGPU با TSL
در حالی که سفر خود را با WebGPU و TSL آسان می کنیم، بیایید با راه اندازی برنامه خود شروع کنیم WebGPURenderer با React Three Fiber و بازآفرینی مقداری کلاسیک نمونههایی از سایهزنها که ممکن است به صورت آنلاین با آنها برخورد کرده باشید یا در پستهای قبلی وبلاگ از آنها عبور کرده باشید.
استفاده از رندر WebGPU
برای دسترسی به ویژگیهای جدید WebGPU، ابتدا باید رندر آن را در پروژه React Three Fiber خود از طریق نمونهسازی کنیم. gl پشتیبانی از Canvas جزء خوشبختانه Canvas در React Three Fiber v9 به روز شده است تا از ناهمزمان پشتیبانی کند gl پشتیبانی کنید 4، یک تابع مورد نیاز از WebGPURenderer نیاز به مقداردهی اولیه ناهمزمان دارد.
قطعه کد زیر نشان می دهد که چگونه پروژه های React Three Fiber که شامل ویژگی های WebGPU در آینده هستند را مقداردهی اولیه می کنیم:
راه اندازی رندر WebGPU در React Three Fiber
1
import * as THREE from 'three/webgpu';8
const renderer = new THREE.WebGPURenderer(props);
در حین مقداردهی اولیه WebGPURenderer می تواند این تصور را ایجاد کند که ما تصمیمی تصادفی برای سه بعدی سازی صحنه خود گرفته ایم فقط برای WebGPU، در واقع اینطور نیست. WebGPURenderer از WebGPU و WebGL پشتیبانی می کند و زمانی که WebGPU در دستگاهی که می خواهد صحنه را ارائه کند در دسترس نباشد به WebGL برمی گردد.
اگر در طول توسعه صحنه خود می خواهید مطمئن شوید که هنوز در WebGL به درستی کار می کند، می توانید به آن بروید forceWebGL: true پارامتر به شما WebGPURenderer.
6
const renderer = new THREE.WebGPURenderer({10
await renderer.init();
نسخه ی نمایشی زیر یک صحنه React Three Fiber با پشتیبانی از WebGPU، شامل چند سایه زن ساده که با TSL نوشته شده است، ایجاد می کند. می توانید آن را مجبور به رندر کردن با WebGL کنید و خودتان ببینید که همان نتیجه را ایجاد می کند، که یکی از مزایای کلیدی نوشتن سایه زن با TSL است.
جابجایی و عادی
به عنوان اولین نمونه پیچیده تر، ما یک کلاسیک ایجاد خواهیم کرد لکه دار کردن با معرفی مقداری جابجایی ارگانیک و دینامیکی کره در مرکز صحنه قبلی. برای بلندپروازانه کردن آن و برجسته کردن مزایای TSL و سیستم گره آن، میتوانیم تمام این کارها را در meshPhongNodeMaterial با نوشتن یک سایه زن برای:
-
در
positionNode، برای تغییر موقعیت رئوس در طول زمان -
در
normalNode، برای تنظیم داده های عادی کره خود بر این اساس به طوری که نقاط برجسته و سایه های کره متحرک ما با جابجایی رئوس آن مطابقت داشته باشد.
همه اینها در حالی که برخی از خصوصیات فیزیکی کلیدی این ماده را حفظ می کند.
برای سازماندهی کد TSL خود، می خواهم:
-
همه آنها را داخل یک بپیچید
useMemo. -
صادرات الف
nodesویژگی که شامل یک فیلد برای هر یک از شیدرهای TSL ما خواهد بود. -
صادرات الف
uniformsدارایی که شامل تمام لباس هایی است که می خواهیم تنظیم کنیم.
سازمان من برای گره های TSL
1
const { nodes, uniforms } = useMemo(() => {2
const time = uniform(0.0);4
const positionNode = Fn(() => {...})();6
const normalNode = Fn(() => {...})();
این یک رویکرد ساختارمندتر برای سازماندهی کد TSL ما در کد React ما ارائه می دهد، و تضمین می کند که ساختارهای کلیدی مانند یونیفرم ها به طور تصادفی دوباره ایجاد نمی شوند.
ما با نوشتن یک سفارشی شروع می کنیم positionNode برای تغییر موقعیت رئوس مش کروی خود پس از نویز. من یک پیاده سازی از Perlin Noise را با استفاده از ترانسپایلری که در این مثال استفاده خواهیم کرد، ترجمه کردم، در انتهای بخش در زمین بازی به طور کامل نمایش داده می شود.
اضافه کردن یک افست برای کره خود با استفاده از positionNode
1
const updatePos = Fn(([pos, time]) => {2
const noise = cnoise(vec3(pos).add(vec3(time))).mul(0.2);3
return add(pos, noise);6
const positionNode = Fn(() => {7
const pos = positionLocal;8
const updatedPos = updatePos(pos, time);
برای متحرک کردن حباب خود در طول زمان، میخواهیم یک را پاس کنیم time به طور مساوی برای تنظیم موقعیت های اوج در طول زمان. با این حال، بین WebGL و WebGPU، یونیفرم ها کمی متفاوت عمل می کنند:
این تفاوت بین راحتی و کنترل کنید توسط TSL از ما پنهان است: برای اعلام استفاده از یونیفرم خود uniform تابع و این صرف نظر از اینکه سایه بان موقعیتی خود را با استفاده از نحو TSL بنویسیم یا نحو GLSL/WGLSL.
اعلام و به روز رسانی یکنواخت TSL
1
const { nodes, uniforms } = useMemo(() => {3
const time = uniform(0.0);16
const { clock } = state;19
uniforms.time.value = clock.getElapsedTime();
اکنون میتوانیم به روش محاسبه مجدد نرمالها کار کنیم. ما کاری مشابه را در Shining a light on Caustics with Shaders و React Three Fiber انجام دادیم، جایی که مجبور شدیم به شدت به اصلاح کد سایه زن منبع منبع متوسل شویم و باید در مورد آن بسیار سخت گیر باشیم تا مطمئن شویم برخی از ویژگی های کلیدی آن را نمی شکنیم. با تشکر از سیستم گره، اکنون فقط باید:
-
گسترش دهید
positionNodeبرای محاسبه نرمال های جابجا شده -
نرمال های محاسبه شده مجدد را از طریق متغیر ارسال کنید
-
الف را اعلام کنید
normalNodeجایی که ما می توانیم این داده ها را برای استفاده توسط مواد برگردانیم
1
const { nodes, uniforms } = useMemo(() => {2
const time = uniform(0.0);3
const vNormal = varying(vec3(), 'vNormal');5
const updatePos = Fn(([pos, time]) => {6
const noise = cnoise(vec3(pos).add(vec3(time))).mul(0.2);7
return add(pos, noise);10
const orthogonal = Fn(() => {11
const pos = normalLocal;12
If(abs(pos.x).greaterThan(abs(pos.z)), () => {13
return normalize(vec3(negate(pos.y), pos.x, 0.0));16
return normalize(vec3(0.0, negate(pos.z), pos.y));19
const positionNode = Fn(() => {20
const pos = positionLocal;22
const updatedPos = updatePos(pos, time);23
const theta = float(0.001);25
const vecTangent = orthogonal();26
const vecBiTangent = normalize(cross(normalLocal, vecTangent));28
const neighbour1 = pos.add(vecTangent.mul(theta));29
const neighbour2 = pos.add(vecBiTangent.mul(theta));31
const displacedNeighbour1 = updatePos(neighbour1, time);32
const displacedNeighbour2 = updatePos(neighbour2, time);34
const displacedTangent = displacedNeighbour1.sub(updatedPos);35
const displacedBitangent = displacedNeighbour2.sub(updatedPos);37
const normal = normalize(cross(displacedTangent, displacedBitangent));39
const displacedNormal = normal42
.select(normal.negate(), normal);43
vNormal.assign(displacedNormal);48
const normalNode = Fn(() => {49
const normal = vNormal;50
return transformNormalToView(normal);
مانند یونیفرم ها، رفتارهای مختلف نیز بین WebGL و WebGPU متفاوت است، و بنابراین دوباره باید به یک تابع TSL تکیه کنیم تا پیچیدگی را از ما پنهان کند.
اکنون که تمام گرههای مورد نیاز خود را به همراه سایهزنهای مربوطهشان اعلام کردهایم، تنها کاری که باید انجام دهیم تا مش ما از آنها استفاده کند این است که هر گره را به نود مربوطه خود در متریال گره خود منتقل کنیم:
استفاده از گره ها در مواد گره TSL
1
const { nodes, uniforms } = useMemo(() => {16
const { clock } = state;18
uniforms.time.value = clock.getElapsedTime();23
icosahedronGeometry args={[1.5, 200]} />26
normalNode={nodes.normalNode}27
positionNode={nodes.positionNode}28
emissive={new THREE.Color('white').multiplyScalar(0.25)}
همانطور که می بینید، روزهایی که باید استفاده می کردیم onBeforeCompile و یک سری از پیوستگی رشته های پیچیده برای تغییر کد سایه زن در موارد Three.js مدت هاست که از بین رفته اند. اکنون میتوانیم به سیستم گرهای تکیه کنیم تا این تغییرات را در مواد موجود بدون تلاش زیاد انجام دهیم یک آشفتگی. دمو زیر خروجی کدی را که در بالا ایجاد کردیم نشان می دهد:
جنس شیشه
در این بخش، بیایید سطح پیچیدگی را بالا ببریم و نه تنها ببینیم که برای ساخت یک ماده از ابتدا چه چیزی لازم است، بلکه همچنین:
-
نحوه پردازش بافت ها به صورت یکنواخت
-
تفاوت ها و مسائل ظریف بین نوشتن مطالب در TSL در مقابل WGSL
در واقع، چیزی که در قسمت قبل غافل شدم به شما بگویم این است uniform تابع فقط انواع زیر را پشتیبانی می کند: boolean | number | Color | Vector2 | Vector3 | Vector4 | Matrix3 | Matrix4, type = null. این می تواند در ابتدا کمی گیج کننده باشد، به خصوص پس از ساخت سایه بان های WebGL و به طور معمول بافت ها را به عنوان یونیفرم برای همه آزمایش های ما منتقل می کنیم. در عوض، ما باید استفاده کنیم texture تابع TSL.
قرار نیست دروغ بگم، اینجا با این یکی کمی گیج شدم texture ویژگی و من هنوز کمی در حال نوشتن این کلمات به دلیل استفاده از آن هستم:
-
برای نمونه، می توانید از آن برای استخراج تکسل در یک UV خاص در کد TSL خود استفاده کنید.
-
از طرف دیگر، متوجه شدم که از آن به عنوان راهی برای انتقال یک بافت به عنوان یکنواخت به یک سایه زن نوشته شده در WGSL استفاده می کنم، زیرا این تنها راه تبدیل من بود.
THREE.Textureبرای تصحیح تایپ کنیدShaderNodeObjectنوع مورد نیاز سایه زن من برای نمونه برداری بافت (از لحاظ فنی atexture_2d).
تکههای کد زیر این دوگانگی را نشان میدهد که چندین بار من را از دست داده است:
بافت TSL در مقابل بافت WGSL
2
const tslTexture = texture(myTexture, uv());5
const wgslTexture = texture(myTexture);6
const sampler = sampler(wgslTexture);
من در این قسمت در مورد بافت ها زیاد صحبت می کنم زیرا آنها هستند ستون فقرات از اجرای سفارشی من از مواد شیشه ای که در سایه زن های انکسار، پراکندگی و سایر جلوه های نور مستند کرده ام. در زیر یک قطعه کد نشان دهنده اجرای آن در TSL است، که می خواهم موارد زیر را برجسته کنم:
-
توانایی تقسیم TSL خود به هر تعداد توابع که دوست دارید با اعلان آنها
Fnتابع اگر میخواهید نامهای خاصی برای متغیرهای خود داشته باشید، میتوانید آرگومانها را بهعنوان آبجکت به آنها ارسال کنید، یا اگر ترجیح میدهید آنها درون خطی باشند، به عنوان عناصر آرایه ارسال کنید. -
توانایی سایهزن شما برای ارجاع به ثابتهای جهانی از کد جاوا اسکریپت/React شما بدون اینکه لزوماً آن را به صورت یکنواخت ارسال کند (مثلاً
lightPosition).
اجرای مواد شیشه انکساری من در TSL
1
const lightPosition = [10, 10, 10];3
const { nodes, uniforms, utils } = useMemo(() => {6
const classicFresnel = Fn(({ viewVector, worldNormal, power }) => {7
const fresnelFactor = abs(dot(viewVector, worldNormal));8
const inversefresnelFactor = sub(1.0, fresnelFactor);9
return pow(inversefresnelFactor, power);12
const sat = Fn(([col]) => {13
const W = vec3(0.2125, 0.7154, 0.0721);14
const intensity = vec3(dot(col, W));15
return mix(intensity, col, 1.265);18
const refractAndDisperse = Fn(({ sceneTex }) => {19
const absorption = 0.5;20
const refractionIntensity = 0.25;21
const shininess = 100.0;23
const noiseIntensity = 0.015;25
const refractNormal = normalWorld.xy26
.mul(sub(1.0, normalWorld.z.mul(0.85)))29
const refractCol = vec3(0.0, 0.0, 0.0).toVar();31
for (let i = 0; i LOOP; i++) {32
const noise = rand(viewportUV).mul(noiseIntensity);33
const slide = float(i).div(float(LOOP)).mul(0.18).add(noise);35
const refractUvR = viewportUV.sub(37
.mul(slide.mul(1.0).add(refractionIntensity))40
const refractUvG = viewportUV.sub(42
.mul(slide.mul(2.5).add(refractionIntensity))45
const refractUvB = viewportUV.sub(47
.mul(slide.mul(4.0).add(refractionIntensity))51
const red = texture(sceneTex, refractUvR).r;52
const green = texture(sceneTex, refractUvG).g;53
const blue = texture(sceneTex, refractUvB).b;55
refractCol.assign(refractCol.add(vec3(red, green, blue)));58
refractCol.assign(refractCol.div(float(LOOP)));60
const lightVector = vec3(65
const viewVector = normalize(cameraPosition.sub(positionWorld));66
const normalVector = normalize(normalWorld);68
const halfVector = normalize(viewVector.add(lightVector));70
const NdotL = dot(normalVector, lightVector);71
const NdotH = dot(normalVector, halfVector);73
const kDiffuse = max(0.0, NdotL);75
const NdotH2 = NdotH.mul(NdotH);76
const kSpecular = pow(NdotH2, shininess);78
const fresnel = classicFresnel({79
viewVector: viewVector,80
worldNormal: normalVector,85
refractCol.add(kSpecular.add(kDiffuse).mul(0.01).add(fresnel))88
return vec3(sat(refractCol));
معادل WGSL کمی متفاوت است، اما هنوز جالب است:
-
می توانید استفاده کنید
codeعملکرد TSL برای تقسیم سایه زن های WGSL شما به بیت های قابل استفاده مجدد. با این حال، نقاط ورودی اصلی نیاز دارند برای استفادهwgslFn. -
مال شما
texture_2dاز تابع WGSL شما یک می شودShaderNodeObject، پس اول باید تماس بگیریدtextureبا بافتی که می خواهید به عنوان استدلال در نظر بگیرید. -
سایه بان های نوشته شده با استفاده از
wgslFnیاglslFnبه نظر می رسد توابع TSL همیشه نیاز دارند که مجموعه آرگومان آنها به عنوان یک شی ارسال شود.
اجرای مواد شیشه انکساری من در WGSL
1
const { nodes, uniforms, utils } = useMemo(() => {4
const sceneTextureUniform = texture(new THREE.Texture());5
const sceneTextureSampler = sampler(sceneTextureUniform);7
const classicFresnel = code(`8
fn classicFresnel(viewVector: vec3f, worldNormal: vec3f, power: f32) -> f32 {9
let fresnelFactor = abs(dot(viewVector, worldNormal));10
let inversefresnelFactor = 1.0 - fresnelFactor;11
return pow(inversefresnelFactor, power);16
fn sat(col: vec3f) -> vec3f {17
let W = vec3(0.2125, 0.7154, 0.0721);18
let intensity = vec3f(dot(col, W));19
return mix(intensity, col, 1.165);24
fn rand(uv: vec2f) -> f32 {25
return fract(sin(dot(uv.xy ,vec2(12.9898,78.233))) * 43758.5453);29
const refractAndDisperseWGSL = wgslFn(`30
fn refract(sceneTex: texture_2d, sampler: sampler, normalWorld: vec3f, viewportUV: vec2f, positionWorld: vec3f, cameraPosition: vec3f) -> vec3f { 31
let absorption = 0.88;32
let refractionIntensity = 0.25;33
let shininess = 100.0;35
let noiseIntensity = 0.015;37
let refractNormal = normalWorld.xy * (1.0 - normalWorld.z * 0.85);38
var refractCol = vec3(0.0, 0.0, 0.0);41
let noise = rand(viewportUV) * noiseIntensity;42
let slide = f32(i)/f32(LOOP) * 0.08 + noise;44
let refractUvR = viewportUV - refractNormal * (slide * 1.0 + refractionIntensity) * absorption;45
let refractUvG = viewportUV - refractNormal * (slide * 2.5 + refractionIntensity) * absorption;46
let refractUvB = viewportUV - refractNormal * (slide * 4.0 + refractionIntensity) * absorption;48
let red = textureSample(sceneTex, sampler, refractUvR).r;49
let green = textureSample(sceneTex, sampler, refractUvG).g;50
let blue = textureSample(sceneTex, sampler, refractUvB).b;52
refractCol += vec3(red, green, blue);55
refractCol = refractCol / f32(LOOP);57
let lightVector = vec3(-10.0, 10.0, -1.5);58
let viewVector = normalize(cameraPosition - positionWorld);59
let normalVector = normalize(normalWorld);61
let halfVector = normalize(viewVector + lightVector);63
let NdotL = dot(normalVector, lightVector);64
let NdotH = dot(normalVector, halfVector);66
let kDiffuse = max(0.0, NdotL);67
let NdotH2 = NdotH * NdotH;68
let kSpecular = pow(NdotH2, shininess);70
let fresnel = classicFresnel(viewVector, normalVector, 25.0);72
refractCol += vec3(kSpecular + kDiffuse * 0.01 + fresnel);74
return vec3(sat(refractCol));75
}`, [classicFresnel, sat, rand],78
const refractAndDisperse = Fn(({ sceneTex }: { sceneTex: ShaderNodeObjectTHREE.TextureNode> }) => {79
return refractAndDisperseWGSL({80
sceneTex: sceneTextureUniform,81
sampler: sceneTextureSampler,
دمو زیر مواد شیشه ای ساخته شده در TSL و کار بر روی لکه ای را که در قسمت اول اجرا کردیم را نشان می دهد.
اکنون که TSL را بهتر میدانیم و میدانیم چگونه از آن برای استفاده و نوشتن سایهزنها برای جدید استفاده کنیم WebGPURenderer، می توانیم توجه خود را به یکی از ویژگی های اصلی WebGPU معطوف کنیم که بسیاری از توسعه دهندگان از آن هیجان زده بودند: شیدرهای محاسباتی.
حالا دو تا هستند خطوط لوله ما می توانیم برای ساخت صحنه های سه بعدی خود استفاده کنیم:
-
را رندر خط لوله، که برای ترسیم گرافیک استفاده می شود، همان چیزی است که در WebGL با آن آشنا هستیم.
-
را خط لوله محاسباتی، برای محاسبات همه منظوره تر که می توانند روی GPU اجرا شوند طراحی شده است 6.
دومی شامل یک مرحله سایه زن محاسباتی (بدون رئوس، بدون شطرنجی، بدون قطعه) است که در آن:
-
ما ورودی تصادفی مانند بافت ها یا بافرها.
-
اعدام گروه های کاری از رشته ها یا فراخوانی های سایه زن برای پردازش این ورودی ها، تابعی که به عنوان سایه زن محاسباتی تعریف می شود.
-
نمایش داده های دریافتی در یک یا چند بافرها. ما می توانیم این بافرهای دریافتی را به خط لوله رندرینگ یا به CPU برگردانیم.

اگرچه سایهزنهای خط لوله رندر ما ملزم به رسم و شطرنجی کردن تماسها هستند، ما میتوانیم شیدرهای محاسباتی خود را به صورت برنامهنویسی فراخوانی/اجرا کنیم و به ما اجازه میدهد تصمیم بگیریم چه زمانی و چند بار آنها را اجرا کنیم.
ایجاد شیدرهای محاسباتی در TSL
بیایید به اولین مثال از نحوه استفاده از خط محاسبه در TSL نگاه کنیم. با استفاده از شیدرهای محاسباتی، ضرب ماتریس را روی GPU انجام می دهیم و نتیجه را از CPU می خوانیم. مانند کد سایه زن قبلی ما، می توانیم شیدر محاسباتی خود را مستقیماً در TSL یا با آن اعلام کنیم wgslFn تابع:
مثالی از یک شیدر محاسباتی که ضرب 2 ماتریس را انجام می دهد
1
const buffer = storage(responseBuffer, 'mat4x4f', count);3
const matrixMult = wgslFn(`4
fn matrixMult(matrix1: mat4x4f, matrix2: mat4x4f, buffer: ptr, read_write>) -> void { 5
let result = matrix1 * matrix2;
سپس، باید تابع TSL شیدر محاسباتی خود را با ارسال آن با استفاده از تابع فراخوانی کنیم .compute روش
1
const matrixMultComputeNode = matrixMult({
دریافت شده computeNode سپس می توان در هر زمان با gl.computeAsync یا gl.compute روش ها در مثال زیر، ما آن را در زمان رندر فراخوانی می کنیم، جایی که نتیجه عملیات خود را نیز با استفاده از آن بازیابی می کنیم gl.getArrayBufferAsync تابع
1
const compute2 = useCallback(async () => {3
await gl.computeAsync(nodes.matrixMultComputeNode);5
const output = new Float32Array(6
await gl.getArrayBufferAsync(buffers.responseBuffer)9
console.log('output:', output);13
}, [nodes.matrixMultComputeNode, gl, buffers.responseBuffer]);
استفاده از شیدرهای محاسباتی برای صحنه سه بعدی شما
در حالی که مثال قبلی یک مورد استفاده عالی برای شیدرهای محاسباتی است، ممکن است بخواهید بدانید که چگونه می توان از این ویژگی جدید در ارتباط با خط لوله رندر استفاده کرد و حتی عملکرد صحنه سه بعدی شما را بهبود بخشید.
یکی از اولین نمونه های عملی استفاده از شیدرهای محاسباتی که در Three.js / React Three Fiber با آن مواجه شدم، از طریق شبکه های نمونه. در اولین آزمایشهایم که شامل مشهای نمونه بود، متوجه شدم که باید موقعیت اولیه آنها را در فضای CPU تنظیم کنم. اما امروز میتوانیم این کار را به صورت زیر به GPU واگذار کنیم:
موقعیت اولیه مش های نمونه سازی شده را با سایه زن محاسباتی تنظیم کنید
3
const { nodes, uniforms } = useMemo(() => {4
const buffer = instancedArray(COUNT, 'vec3');6
const computeInstancePosition = wgslFn(`8
buffer: ptr, read_write>, 12
let gridSize = u32(count);13
let gridWidth = u32(sqrt(count));14
let gridHeight = (gridSize + gridWidth - 1u) / gridWidth;16
if (index >= gridSize) {20
let x = index % gridWidth;21
let z = index / gridWidth;24
let worldX = f32(x) * spacing - f32(gridWidth - 1u) * spacing * 0.5;25
let worldZ = f32(z) * spacing - f32(gridHeight - 1u) * spacing * 0.5;27
buffer[index] = vec3f(worldX, 0.0, worldZ);31
const computeNode = computeInstancePosition({37
const positionNode = positionLocal.add(buffer.element(instanceIndex));47
const compute = useCallback(async () => {49
await gl.computeAsync(nodes.computeNode);53
}, [nodes.computeNode, gl]);56
if (!meshRef.current) return;63
args={[undefined, undefined, COUNT]}67
icosahedronGeometry args={[0.3, 16]} />69
emissive={new THREE.Color('white').multiplyScalar(0.15)}71
positionNode={nodes.positionNode}
که می توان با نمودار متحرک زیر نشان داد که تمام قسمت های هر خط لوله با هم کار می کنند:
0.1
0.9
0.1
0.3
0.1
0.1
0.7
0.9
0.2
0.1
0.9
0.1
0.3
0.1
0.1
0.7
0.9
0.2
0.1
0.9
0.1
0.3
0.1
0.1
0.7
0.9
0.2
-
ابتدا یک بافر ایجاد می کنیم، آرایه ای از بافر به اندازه
COUNT * 3، از طریقinstancedArrayبا تعیین تعداد نمونه هایی که می خواهیم رندر کنیم و نوع، در این مورد، avec3. -
در ادامه شیدر محاسباتی را تعریف می کنیم. من در اینجا از WGSL استفاده می کنم زیرا برای من قابل خواندن تر است و به هر حال صحنه ما فقط از طریق WebGPU اجرا می شود. این سایه زن خاص هر نمونه را در یک شبکه در امتداد محورهای x و z با استفاده از موقعیت قرار می دهد
instanceIndexاز نمونه سپس موقعیت های به دست آمده در بافر آرایه ای که به عنوان آرگومان ارسال کردیم، نوشته می شود. -
شیدر محاسباتی را اجرا می کنیم
COUNTبار، در نتیجه ایجادCOUNTthread ها، که هر نخ مسئول اجرای سایه زن برای یک نمونه خاص است. این مثال شیدر محاسباتی را اجرا می کند یک بار در طول رندر (از این رو چرا انیمیشن خط لوله محاسباتی در نمودار بالا در نقطه ای متوقف می شود).
-
در نهایت، میتوانیم بافر را در یک مقدار مشخص بخوانیم
instanceIndexو موقعیت ذخیره شده حاصل را به نمونه های خود اضافه کنیدpositionLocalاز طریقpositionNode. با TSL و توابع کمکی آن، میتوانیم به راحتی رفتار هر نمونه را با استفاده از همان تغییر دهیمinstanceIndexشاخص -
در صورت لزوم، داده های بافر یا برخی از جنبه های آن را می توان از طریق vering به هر “گره مرتبط با قطعه” ارسال کرد.
موقعیت های هر نمونه را راه اندازی و به روز کنید
1
const updatePosition = wgslFn(`8
let waveAmplitude = 0.5;9
let waveFrequencyX = 0.75;10
let waveFrequencyZ = 0.75;12
let waveOffset = sin(position.x * waveFrequencyX + position.z * waveFrequencyZ - time * waveSpeed) * waveAmplitude;13
let waveOffset2 = sin(-position.x * waveFrequencyX + position.z * waveFrequencyZ - time * waveSpeed) * waveAmplitude;14
let newY = position.y + (waveOffset + waveOffset2) / 2.0;16
return vec3f(position.x, newY, position.z);20
const positionNode = updatePosition({21
position: positionLocal.add(buffer.element(instanceIndex)),
نسخه ی نمایشی زیر خط لوله محاسباتی و رندر خط لوله ما را نشان می دهد که دست در دست هم کار می کنند تا این مش متحرک زیبا از 1024 مش را ارائه دهد.
اندازه و ارسال گروه کاری
در مثال قبلی فقط تنظیم کردیم count استدلال به شیدرهای محاسباتی ما .compute روشی که در واقع می تواند یک آرگومان اضافی بگیرد: workgroupSize.
ارسال این آرگومان اضافی به WebGPU میگوید که چند thread/shader فراخوانی متعلق به یک گروه کاری است و دارای ویژگیهای زیر است:
-
این استدلال شکلی دارد
[x, y, z]کجاx،y، وzابعاد گروه کاری در هر جهت مش سه بعدی است. -
شما می توانید تعداد کل موضوعات در هر گروه کاری را با ضرب هر یک در بدست آورید
x،y، وz. برای مثال،[64, 1, 1]و[8, 8, 1]هر دو گروه کاری ما را از 64 رشته تعریف می کنند. -
اندازه گروه کاری به نحوه نوشته شدن سایه زن محاسباتی و فرمت داده/بافرهای شما بستگی دارد. برای بافرهای 1 بعدی، [64, 1, 1] یک انتخاب طبیعی است، در حالی که برای بافرهای دو بعدی مانند بافت، آن را به عنوان مشخص کنید [8, 8, 1] ممکن است مناسب تر باشد.
-
حداکثر محدودیت 256 رشته برای هر گروه کاری وجود دارد. می توانید تنظیم را امتحان کنید
[512, 1, 1]و تمام چیزی که دریافت خواهید کرد یک هشدار از طرف WebGPU خواهد بود. -
در Three.js به صورت پیشفرض روی میدهد
[64, 1, 1 ]7.
بنابراین، در تظاهرات قبلی ما، با توجه به COUNT = 1024 در مواردی، ما باید اجرا کنیم 1024 / 64 = 16 گروه های کاری به طور کلی، تعداد گروه های کاری است ceil(COUNT / workgroupSize.x) در مورد 1D
اکنون یک “چیز سوم مخفی” نیز وجود دارد به نام اندازه حمل و نقل. به طور پیش فرض، زمانی که استفاده می کنید .compute روش، شما لازم نیست نگران این باشید زیرا TSL/Three اندازه حمل و نقل را برای شما محاسبه می کند count و اندازه گروه کاری تنظیم شده (یا پیش فرض). 8
با این حال، اگر میخواهید از یک رویکرد دستیتر برای دانستن تعداد گروههای کاری برای شروع استفاده کنید، میتوانید آن را به صورت دستی با استفاده از .computeKernel در عوض، که اخیراً به پایگاه کد Three.js اضافه شده است.
مثال استفاده از computeKernel
1
computeNode = computeInstancePositions().computeInstancePosition([8, 8, 1]);2
gl.compute(computeNode, [4, 4, 1]);
در مورد ما، از آنجایی که ما 1024 نمونه داریم:
-
می توانیم از اندازه برای ارسال استفاده کنیم
[16, 1, 1]، که منجر به ایجاد 1024 موضوع می شود (64 * 16 * 1 * 1 * 1 * 1)، یک مورد برای هر نمونه، که برای مورد استفاده ما کافی است. -
برای ابعاد دو بعدی، می توانیم آن را به صورت تعریف کنیم
[8, 8, 1]با اندازه گروه کاری[4, 4, 1]که به ما می داد4 * 8 * 4 * 8 * 1 * 1 = 1024

به طور خلاصه:
-
workgroupSizeتعداد رشته ها در هر گروه کاری است. -
dispatchSizeتعداد گروه های کاری در هر بعد است. -
Total threads = (workgroupSize.x * dispatchSize.x) × (workgroupSize.y * dispatchSize.y) × (workgroupSize.z * dispatchSize.z).
ما در هیچ نمونهای از اندازه ارسال استفاده نخواهیم کرد، اما فکر کردم ذکر آن مهم است زیرا به ما در مورد عملکرد داخلی خط لوله محاسباتی اطلاع میدهد.
تغذیه ذرات با شیدرهای محاسباتی
در قسمتهای اول این مقاله، در حین بررسی پتانسیل سایهزنهای محاسباتی برای کارهای سهبعدی، به اصول WebGPU/TSL و سیستم گرههای آن پرداختیم. استفاده از آنها با هم instancedMesh تنها یکی از موارد استفاده زیاد برای شیدرهای محاسباتی است، و من میخواستم بخشهای بعدی این مقاله را به مثالهای کاربردیتری اختصاص دهم که نه تنها میتوانند جاهای خالی را هنگام انتقال کار خاص به TSL پر کنند، بلکه کاندیدهای خوبی برای استفاده از خط لوله محاسباتی باشند.
اولین مورد استفاده عملی که به نظرم جالب بود و مزایای زیادی را پس از انتقال آنها به TSL و استفاده از شیدرهای محاسباتی نشان داد. ذرات.
در سال 2022 مقاله ای نوشتم که بسیاری از جنبه های ذرات را پوشش می داد. در میان آنها، مورد علاقه من ذرات GPGPU بود. سپس از یک Frame Buffer Object (FBO) برای رندر کردن به یک بافت استفاده کردیم خارج از صفحه نمایش پر شده با داده های موقعیت هر ذره در یک قاب معین. پس از به روز رسانی هر یک از این موقعیت ها از طریق یک قطعه سایه زن، بافت به دست آمده را به سایه زن نهایی منتقل می کنیم تا موقعیت های جدید را بدست آوریم و ذرات را رندر کنیم.

امروزه همه اینها را می توان به شیدر کامپیوتر منتقل کرد! نه فقط بهروزرسانی موقعیت، بلکه نمونهسازی خود ذرات، که از نظر تاریخی همیشه روی CPU انجام میدادیم.
راه اندازی سیستم ذرات ما با Compute Shaders
بیایید با تنظیم صحنه ای شروع کنیم که نحوه ایجاد یک سیستم ذرات را با استفاده از خط لوله محاسباتی نشان می دهد.
استفاده خواهیم کرد sprite به عنوان “مش” ذره ما به همراه موادی که با آن مطابقت دارد: spriteNodeMaterial. شبیه به instancedMesh، این ساخت و ساز طول می کشد count prop که مطابق با تعداد ذراتی است که می خواهیم روی صفحه نمایش دهیم. مواد گرهای آن میتواند بسیاری از خواصی را که قبلاً در این مقاله به آنها اشاره کردیم، داشته باشد، مانند positionNode یا colorNode، که به ما امکان می دهد سیستم ذرات خود را با سایه زن سفارشی کنیم.
استفاده از sprite و spriteNodeMaterial
8
blending={THREE.AdditiveBlending}
اگر بخواهیم این را همانطور که هست ارائه کنیم، همه ذرات به آن تبدیل می شوند (0, 0, 0). می توانیم راه اندازی کنیم instancedArray در vec3 که تمام موقعیت های شروع را برای ذرات ما حفظ می کند. سپس می توانیم از یک شیدر محاسباتی استفاده کنیم که آن را کپی می کنیم computeInit برای مقداردهی اولیه ذرات در موقعیت مورد نظر در این بافر.
ComputeShader موقعیت های شروع سیستم ذرات ما را تنظیم می کند
1
const spawnPositionsBuffer = instancedArray(COUNT, 'vec3');2
const spawnPosition = spawnPositionsBuffer.element(instanceIndex);5
fn hash(index: u32) -> f32 {6
return fract(sin(f32(index) * 12.9898) * 43758.5453);10
const computeInitWgsl = wgslFn(13
spawnPositions: ptr, read_write>, 17
let h1 = hash(index + 1u);18
let h2 = hash(index + 2u);20
let distance = sqrt(h0 * 4.0);21
let theta = h1 * 6.28318530718; // 2 * PI22
let phi = h2 * 3.14159265359; // PI24
let x = distance * sin(phi) * cos(theta);25
let y = distance * sin(phi) * sin(theta);26
let z = distance * cos(phi);28
spawnPositions[index] = vec3f(x, y, z);34
const computeNode = computeInitWgsl({35
spawnPositions: spawnPositionsBuffer,39
const positionNode = Fn(() => {40
const pos = spawnPosition;
در قطعه کد بالا:
-
ما تمام موقعیت های ذرات خود را به طور تصادفی در یک کره به شعاع مقداردهی اولیه می کنیم
2. بافر ذخیره ذرات ما در حین رندر پر می شود. -
سپس بردار موقعیت هر ذره از یک نمونه معین را حل کرده و عبور می دهیم
positionNodeاز مال ماspriteNodeMaterialو از طریق اهرمinstanceIndex.
جذب کننده ها با شیدرهای محاسباتی
فقط با مقداردهی اولیه، اکنون یک مورد استفاده خوب برای خط لوله محاسباتی داریم: آنچه قبلاً روی CPU انجام می شد اکنون روی GPU انجام می شود. با این حال، ما می توانیم بهتر عمل کنیم!
در مورد ذرات GPGPU، میتوانیم کل فرآیند نوشتن FBO و بافت را جایگزین کنیم شیدر تک کامپیوتر که روی آن می دویم هر فریم. این امر اجرای یک سیستم ذرات را در جایی که موقعیت یک ذره در زمان تعیین می کند، بسیار ساده می کند t به موقعیت قبلی آن در یک زمان معین بستگی دارد t - dt، یعنی توسط یک معادله پارامتری به عنوان جاذبه تعریف می شود.
در اینجا یک بافر ذخیره سازی اضافی به نام اضافه می کنیم offsetPosition که شامل تمام موقعیت های ذرات ما در یک قاب معین خواهد بود. این موقعیت های افست به طور مداوم بر اساس مقادیر قبلی خود به روز می شوند همانطور که ما سایه بان محاسبه را برای پر کردن آنها در هر فریم می نامیم.
به روز رسانی موقعیت ذرات ما در هر فریم با سایه زن محاسباتی
1
const { nodes } = useMemo(() => {2
const spawnPositionsBuffer = instancedArray(COUNT, 'vec3');3
const offsetPositionsBuffer = instancedArray(COUNT, 'vec3');5
const spawnPosition = spawnPositionsBuffer.element(instanceIndex);6
const offsetPosition = offsetPositionsBuffer.element(instanceIndex);10
const thomasAttractor = wgslFn(`11
fn thomasAttractor(pos: vec3) -> vec3 { 20
let dx = (-b * x + sin(y)) * dt;21
let dy = (-b * y + sin(z)) * dt;22
let dz = (-b * z + sin(x)) * dt;24
return vec3(dx, dy, dz);28
const computeNodeUpdate = Fn(() => {29
const updatedOffsetPosition = thomasAttractor({30
pos: spawnPosition.add(offsetPosition),32
offsetPosition.addAssign(updatedOffsetPosition);35
const positionNode = Fn(() => {36
const pos = spawnPosition.add(offsetPosition);53
gl.compute(nodes.computeNodeUpdate);
سپس می توانیم موقعیت افست یک نمونه داده شده را با استفاده از آن بدست آوریم instanceIndex و آن را به موقعیت تخم ریزی مربوطه خود اضافه کنید. نمودار زیر این مثال از یک خط لوله محاسباتی در حال اجرا بر روی هر فریم برای به روز رسانی یک سیستم ذرات را نشان می دهد:
در مورد خود صحنه، نسخه آزمایشی زیر سایهزن محاسباتی مورد استفاده برای نمونهسازی سیستم ذرات ما را با یکی که موقعیت آنها را در طول زمان بهروزرسانی میکند ترکیب میکند. در این مورد خاص، attractorFunction نشان دهنده یک جاذبه توماس است.
به عنوان کسی که عاشق پس پردازش است، از اینکه متوجه شدم افکتهای TSL آنطور که فکر میکردم مستند نشده بودند، ناامید شدم و صحنههایی را از React Three Fiber استاندارد رندر میکردم. @react-three/postprocessing در ابتدا کمی سخت است من مجبور شدم به چند نمونه از مخزن نمونه Three.js نگاه کنم و با آزمون و خطا بفهمم که چگونه می توانم آنها را بر روی تنظیمات React Three Fiber موجودم کار کنم.
هدف این بخش این است که این مهاجرت را برای افرادی که شروع به کار می کنند کمی آسان تر کند، در حالی که یک مورد عملی برای سایه زن های محاسباتی با اثرات پس از پردازش ارائه می دهد.
تنظیم افکت های سفارشی پس از پردازش با TSL React Three Fiber
مشابه آنچه در قسمت دوم این مقاله ایجاد کردیم، من همچنین دوست دارم گره TSL پس از پردازش خود را در داخل سازماندهی کنم. useMemo.TSL چندین تابع کمکی را ارائه می دهد که ما از آنها برای راه اندازی خط لوله پس از پردازش خود استفاده خواهیم کرد:
-
pass، که گره ای است که یک پاس رندر جدید ایجاد می کند 9 -
getTexture، روش ازPassNodeکه بافت را برای نام منبع داده شده برمی گرداند. به طور پیشفرض، میتواند بافت منتشر یا بافت عمق را برگرداند. -
setMRT، روش ازPassNodeکه به شما امکان می دهد به اهداف رندر اضافی ارجاع دهید (از این رو نام MRT -> اهداف رندر چندگانه)
سازماندهی گره های من برای پردازش پست
1
const { textures } = useMemo(() => {2
const scenePass = pass(scene, camera);3
const diffuse = scenePass.getTextureNode('output');4
const depth = scenePass.getTextureNode('depth');6
const outputNode = diffuse;
سپس شما باید:
-
یک نمونه جدید از
THREE.PostProcessingدر برابر رندر WebGPU ما. -
آن را تنظیم کنید
ouputNodeویژگی به هر پاسی که می خواهیم، و همچنین نمونه حاصل را در React ref برای ارجاع بعدی ذخیره کنید. -
تماس بگیرید
renderروش نمونه پس پردازش ما
قسمت سخت این بود که بفهمیم کجا باید تماس بگیریم render در وهله اول. میدونستم وارد میشه useFrame، اما در نگاه اول هیچ کاری انجام نداد. برای اینکه این کار انجام شود، باید آن را تنظیم کنیم renderPriority استدلال از useFrame به 1این اثر زیر را دارد:
ارائه نتیجه پس از پردازش ما
2
const postProcessing = new THREE.PostProcessing(gl);3
postProcessing.outputNode = outputNode;4
postProcessingRef.current = postProcessing;6
if (postProcessingRef.current) {7
postProcessingRef.current.needsUpdate = true;8
postProcessingRef.current.outputNode = customPass(outputNode);12
postProcessingRef.current = null;17
if (postProcessingRef.current) {18
postProcessingRef.current.render();
اکنون میتوانیم یک پاس سفارشی پس از پردازش ایجاد کنیم که میتواند با سایهزنهای TSL یا WGSL/GLSL (بسته به آنچه دوست داریم) پیادهسازی شود. کد قالبی که در کارم استفاده کردم اینجاست:
نمونه ای از گره افکت سایه زن سفارشی که در TSL نوشته شده است
1
class CustomEffectNode extends THREE.TempNode {2
constructor(inputNode) {4
this.inputNode = inputNode;8
const inputNode = this.inputNode;10
const effect = Fn(() => {11
const input = inputNode;13
return vec4(input.r.add(0.25), input.g, input.b, 1.0);16
const outputNode = effect();22
const customPass = (node) => nodeObject(new CustomEffectNode(node));
قطعه کد بالا می تواند به عنوان نقطه شروع برای کار پس از پردازش خود استفاده شود. در اینجا، سایهزن افکتهای ما، کانال رنگ قرمز بافت ورودی را افزایش میدهد، که اگر همه چیز به درستی تنظیم شود، باید یک صحنه رنگی ایجاد کند.
جلوه های پس از پردازش با Compute Shaders
اکنون که می دانیم چگونه با پردازش پس پردازش React Three Fiber با TSL کار کنیم، بیایید نمونه ای بسازیم که از خط لوله محاسباتی به همراه افکت سایه زن سفارشی برای استایل دادن به صحنه ما استفاده می کند. برای این بخش، ما مجدداً مستقر خواهیم کرد اثر کانتور من پس پردازش به سبک Moebius و سایر شیدرهای سبک شده را معرفی کردم که مبتنی بر فیلتر تشخیص لبه به نام “فیلتر Sobel” هستند.
به عنوان یادآوری، برای اینکه فیلتر تشخیص لبه ما کار کند، ابتدا به موارد زیر نیاز داریم:
-
عمق بافت صحنه. این ما را از تفاوتهای شدید در عمق که میخواهیم خطوطی برای تشخیص پیشزمینه و پسزمینه ترسیم کنیم، آگاه میکند.
-
بافت صحنه معمولی این ما را از هرگونه تغییر جهت ناگهانی برای هر شبکه ای در صحنه ما مطلع می کند. به طور مشابه، ما می خواهیم خطوطی را برای برجسته کردن جزئیات در مش ترسیم کنیم.

ترکیب دو نرخ تغییر/ گرادیان (نرمال و عمق) یک مقدار کلی تغییر را به ما می دهد، یعنی قدرت لبه در یک موقعیت معین روی صفحه نمایش. سپس میتوانیم این نیرو را با شدت یا ضخامت خطوط مرتبط کنیم.
در توضیحات اصلی من در مورد موضوع، من این محاسبات را مستقیماً در اثر نهایی انجام دادم. اما امروز می توانیم:
-
از یک شیدر کامپیوتر برای پر کردن a استفاده کنید
StorageTextureبا قدر در هر UV. -
این را پاس کن
StorageTextureبه عنوان استدلالی برای اثر نهایی ما، که اکنون فقط مسئول تفسیر این داده ها برای ترسیم خطوط نهایی است. -
اگر صحنه پویا است، یک یا هر فریم را با شیدر محاسباتی تماس بگیرید.

سایهزن قطعه اثر
در مرحله محاسبه:
1
const storageTexture = new THREE.StorageTexture(2
window.innerWidth * window.devicePixelRatio,3
window.innerHeight * window.devicePixelRatio
1
const posX = instanceIndex.mod(2
int(window.innerWidth * window.devicePixelRatio)5
const posY = instanceIndex.div(window.innerWidth * window.devicePixelRatio);
-
ما هم می توانستیم از آنها استنباط کنیم
UVمختصاتی که برای محاسبه عمق و نرمال مورد نیاز است.
1
const fragCoord = uvec2(posX, posY);3
float(fragCoord.x).div(float(window.innerWidth * window.devicePixelRatio)),4
float(fragCoord.y).div(float(window.innerHeight * window.devicePixelRatio))
1
const magnitude = GDepth.add(GNormal);2
textureStore(storageTexture, fragCoord, vec4(magnitude, 0.0, 0.0, 1.0));
در مرحله بعد، ما باید شیدر محاسباتی خود را طوری تنظیم کنیم که در هر پیکسل از صفحه نمایش ما یک رشته ایجاد کند. window.innerWidth * window.devicePixelRatio * window.innerHeight * window.devicePixelRatio. سپس می توانیم آن را با استفاده از gl.compute در مال ما useFrame، بنابراین اطمینان حاصل می کند که بافت حاوی داده های بزرگی ما را در هر فریم به روز می کند تا هر تغییر در زاویه دوربین یا هر حرکت چشم را کنترل کند.
دمو زیر شامل ترکیبی از موارد زیر است:
-
خط لوله محاسباتی ما، فیلتر سوبل ما را اجرا می کند و مقادیر حاصل را در یک بافت ذخیره می کند.
-
خط لوله رندر ما، شروع صحنه ما و اعمال افکت کانتور سفارشی ما بر اساس داده های بزرگی ذخیره شده در بافت است.
افکار نهایی و آینده
من می توانم در مورد هر یک از مثال ها و موضوعات ارائه شده در این پست یک مقاله جداگانه بنویسم. با این حال، نشان دادن آنها با هم طبیعیتر به نظر میرسد تا بتواند به عنوان یک راهنما برای هر کسی که میخواهد از Three.js/React Three Fiber کلاسیک به این روش جدید ساخت سهبعدی برای وب حرکت کند، عمل کند.
امیدوارم این به برخی از سوالاتی که ممکن است در مورد WebGPU و TSL داشتید پاسخ دهد و جنبههایی را که ممکن است فوراً از مستندات و مثالهای موجود آشکار نباشد، شفافسازی کند.
اگر بخواهم چند فکر از خودم را در این مرحله از سفر یادگیری ام یادداشت کنم، آنها عبارتند از:
-
سیستم گره در حال تغییر زندگی است. من در واقع من آن را دوست دارم من فکر می کنم بسیاری از توسعه دهندگان دیگر با من در این مورد موافق هستند.
-
خلاصه TSL زیاد از همه چیز دور است، که خوب است زیرا بسیار مناسب برای مبتدیان است و مانع ورود توسعه سایهبان را کاهش میدهد، اما در مواقعی که کنترل را برای موارد استفاده پیشرفتهتر از دست میدهیم، میتواند دردسرساز شود.
-
انتزاع TSL، اگرچه برای کار سریع و تمرکز بر ساخت عالی است، ممکن است مرا وادار کند که کار با WebGPU را خارج از چارچوب Three.js/React Three Fiber شروع کنم. پروژههایی مانند TypeGPU وجود دارند که کار با WebGPU را در دسترستر میکنند در حالی که ویژگیهای اصلی آن را کاملاً از دید توسعهدهندگان پنهان نمیکنند.
-
TSL هنوز در حال پیشرفت است، بسیاری از ویژگی های آن ضعیف هستند یا اصلاً مستند نیستند. بیشترین دلخوری من از او در همین جاست. من اغلب متوجه میشدم که مجبور بودم استفاده از یک تابع را استنباط کنم یا به عنوان مثال در جایی در مخزن Three.js جستجو کنم، که سرعتم را کاهش داد. میخواهم مشخصات کاملی را ببینم که در مقطعی نوشته شده باشد تا حدس و گمان را از استفاده از آن حذف کنم و بهترین روشها را مستند کنم.
در عین حال، از آنجایی که برخی از این جزئیات تکمیل شدهاند، من همچنان در حال نوشتن سایهزنان در GLSL خواهم بود، مگر اینکه مورد استفاده خاصی داشته باشم که در آن به سایهزنهای محاسباتی نیاز داشته باشم، مانند تجربیات اخیرم با چشمانداز رویهای، که بسیار هیجانزده هستم که درباره روز آماده شدن این پروژه بیشتر به شما بگویم. اگر برای به کارگیری دانش جدید TSL/WebGPU خود به الهام بیشتری نیاز دارید، در اینجا لیستی از تمام صحنه های دیگری که در تابستان ایجاد کردم آورده شده است. می توانید سعی کنید خودتان آنها را بازتولید کنید تا با TSL و WebGPU راحت تر شوید. هر آنچه شما نیاز دارید در این مقاله ارائه شده است:
-
WebGPU WebKit برای Safari 26.0 پشتیبانی از WebGPU را اضافه می کند.
-
مقایسه عملکرد WebGPU و WebGL در بازی گودو
-
همچنین بستگی به این دارد که آیا از شیدرهای محاسباتی استفاده می کنید یا خیر، زیرا آنها یک ویژگی فقط WebGPU هستند
-
Async GL prop – React Three Fiber Docs
-
یونیفرم های WebGPU – WebgpuFundamentals.org
-
WebGPU API – MDN
-
https://github.com/Spiri0/three.js/blob/aa60b0cca42da40b489070a135d4a23459bad59e/src/nodes/gpgpu/ComputeNode.js#L214
-
https://github.com/Spiri0/three.js/blob/aa60b0cca42da40b489070a135d4a23459bad59e/src/renderers/webgpu/WebGPUBackend.js#L1356-L1387
-
https://github.com/mrdoob/three.js/blob/9e7a65c17bc3832798103190df8f674b9acca073/src/nodes/display/PassNode.js#L162
-
در دست گرفتن حلقه رندر – React Three Fiber docs




