چرا طراحان بازی باید انیمیشن یاد بگیرند؟

فرناز حقیقت

۱۳۹۵/۱۲/۰۹

روابط عمومی
b

آنالیز: صنعت بازی سازی لهستان – قسمت سوم (آخرین قسمت)

رهام سجادی

۱۳۹۵/۱۲/۱۳

عضو تحریریه
b

سرگردان در بازی؛ آشنایی با بازی‌های جهان باز

رهام سجادی

۱۳۹۵/۱۱/۲۸

عضو تحریریه
b

خنک بنوشید؛ گزارش جنجالی پازل از برگزاری رویداد گیمیکس و فرمول محرمانه معجون بازی‌سازی

پازل

۱۳۹۵/۱۱/۲۴

آشنایی با ارزش طول عمر کاربر و نحوه محاسبه آن

حسین مزروعی

۱۳۹۵/۱۱/۱۶

آنالیز: صنعت بازی‌سازی لهستان - قسمت اول

تحریریه گیمولوژی

۱۳۹۵/۱۱/۰۴

روایت در بازی‌های رایانه‌ای – قسمت دوم

ماکان علیخانی

۱۳۹۵/۱۰/۲۵

عضو تحریریه
b

گسترش نگاه؛ طعم اولین معجون بازی سازی

علیرضا محمدی

۱۳۹۵/۱۰/۲۱

سردبیر
b

روایت در بازی‌های رایانه‌ای – قسمت اول

ماکان علیخانی

۱۳۹۵/۱۰/۰۶

عضو تحریریه
b

مدلسازی با Raymarching و میادین فاصله در یونیتی

Raymarching یک روش برای ارائه گرافیک کامپیوتری است، اما هنوز به پتانسیل کامل نرسیده است. در Raymarching نیازی به خط لوله گرافیکی نیست و از آن معمولا برای رندر کردن بافت حجمی(Volume Textures) و Heightmaps و سطوح تحلیلی(Analytic Surfaces) استفاده می‌شود. امروزه اکثر بازی‌ها از OpenGL یا Direct3D (DirectX) استفاده می‌کنند تا به وسیله‌ی شتاب سخت‌افزاری کارت گرافیکی، چندضلعی‌ها را رسم کنند. رایانه‌ها می‌توانند میلیون‌ها مثلث را به صورت 60 فریم در ثانیه را پردازش کنند که این حیرت آور است! Raymarching به خوبی APIهای گرافیکی شناخته نمی‌شود. اما چقدر جزئیات را می‌توان فقط با 2 مثلث به دست آورد؟

هدف من از این مطلب این است که به شما نشان دهم این روش قدیمی‌تر رندر، می‌تواند به بازی بازگردد و با استفاده از پردازش موازی و تکنیک‌های محاسبه شده بهینه‌سازی شود! GPU در حال حاضر بسیار قدرت‌مند است و از آن برای شتاب فیزیک و حتی یادگیری هوش مصنوعی عمیق استفاده می‌شود. حال وقت آن است که از آن برای استفاده بیش از فقط خط لوله گرافیکی استفاده شود!

https://github.com/smkplus/UnityRayMarching

دو مثلث، با جزئیات بی‌نهایت!!!

Raymarching روش ریاضی برای رندر کردن است. با دایره‌های فاصله (فاصله از یک نقطه به یک ابتدایی)، مراحل ثابت (معمولا با رندر حجمی استفاده می‌شود) و Root-Finders (یک رویکرد ریاضی) انجام می‌شود.

 https://www.shadertoy.com/view/ld3Gz2

به طور معمول ایجاد یک صحنه مانند تصویر بالا، به ابزار مدل‌‌سازی (Maya و Blender و 3D Max) و ابزار بافت‌دهی (Substance Designer ، Painter) نیاز دارد؛ اما این صحنه با ریاضی ایجاد و با Raymarching رندر شده است. در این روش شما محدود به این نیستید که چند مثلث می‌توانید رندر کنید. با این حال Raymarching راه‌حل همه مشکلات ما نیست. این روش به سبک خود آهسته است و من فکر می‌کنم که باید در کنار چندضلعی‌ها استفاده شود. اجازه بدهید در کد توضیح بدهم.


در اینجا تابعی برای برگرداندن فاصله‌ی بافر عمق دوربین وجود دارد.


float GetDistanceFromDepth(float2 uv, out float3 rayDir)

{

//آوردن مختصات یو وی در فضای درست، برای انجام محاسبه ماتریس ریاضی

    float2 p = uv * 2.0f - 1.0f; // -از 1 به 1

//از این فاکتور برای تبدیل عمق به فاصله استفاده کنید
// این فاصله ی کمرا اوریجین به مختصات یو وی متناظر است
//مختصات نزدیک پِلین هست

    float3 rd = mul(_invProjectionMat, float4(p, -1.0, 1.0)).xyz;
//یک سری متغییر برای تنظیم فاصله های پراجکشن انجام میدیم
//وای و زد به ترتیب فاصله های دور و نزدیک هستند

    float a = _ProjectionParams.z / (_ProjectionParams.z - _ProjectionParams.y);
    float b = _ProjectionParams.z * _ProjectionParams.y / (_ProjectionParams.y - _ProjectionParams.z);
    float z_buffer_value =  tex2D(_CameraDepthTexture, uv).r;

// مقادیر بافر زد به ترتیب زیر توزیع شده اند
    // z_buffer_value =  a + b / z 
//بنابراین،ماتریس زیر معکوس است، برای محاسبه عمق خط دید

    float d = b / (z_buffer_value-a);
//
// این تابع همچنین مسیر پرتو را باز می کند، بعدا استفاده می شود (بسیار مهم)
//این تابع مسیر اشعه را بر می گرداند که بعداً از آن استفاده می شود(بسیار مهم است)

    rayDir = normalize(rd);

    return d;
}

 

چگونه Raymarching را به بازی‌های‌مان اضافه کنیم؟
ترکیب دو روش رندر سخت نیست، اما اول شما باید تفاوت‌شان را درک کنید.

    • Raymarching صد در صد دقیق نیست. با استفاده از میدان‌های فاصله ما را به سطحی که می‌خواهیم رندر کنیم، نزدیک می‌کند؛ اما تقریبا هرگز به درستی نمی‌توانیم به دنبال فاصله‌ی درست آن بگردیم.
    • رندر چند ضلعی (با perspective) شامل استفاده از ماتریس طرح‌ریزی(projection matrix) است. این عمق است، فاصله نیست.

به منظور ترکیب دو روش رندر ، معمولا ساده‌ترین راه شروع با چندضلعی‌ها است و بنا به دلایل مختلف با Raymarching به پایان می‌رسد. آزمایش عمیق با یک بافر فاصله سخت است و شما خود را به اشیای جامد محدود می‌کنید. پس از پایان دادن به تمام رندر، مرحله فاز Raymarching باید انجام شود. (شبیه به نحوه چیدن اجسام جامد قبل از آن که شفاف باشد). بیایید در مورد آماده‌سازی عمق بافر صحبت کنیم و آن را به یک بافر فاصله بدهیم.

آن چه که در این‌جا رخ می‌دهد، این است که من از معکوس ماتریس طرح‌ریزی استفاده می‌کنم تا بفهمم که کدام مختصات UV (تبدیل شده به ( [-1,-1] -> [1,1] ) در فاصله نزدیک plane قرار دارد (1-،x،y). در این مرحله، من از ماتریس دوربین استفاده نکرده‌ام، بنابراین فرض بر این است که دوربین در origin ([0,0,0]) است. طول این مختصات با مختصات UV متفاوت است.فاصله نزدیک plane باید با مختصات UV [0.5،0.5] مساوی باشد.

پس از گرفتن این اعداد، جهت متغیر Ray را تنظیم می‌کنیم .این کار لازم است چون Raymarching توسط پرتاب اشعه کار می‌کند.

Raymarching  چطور کار می‌کند

در حال حاضر همه چیزهایی را که لازم است شروع کنیم داریم. با فاصله از بافر عمق، می‌توانیم تقاطعات را اداره کنیم. با ماتریس طرح‌ریزی معکوس، اشعه‌های صحیح را محاسبه کردیم تا با میدان دید دوربین‌مان منطبق باشد. چه چیز باقی مانده است؟ موقعیت دوربین و سپس روند انجام می‌شود. خوشبختانه این آسان است.

این کد به موقعیت دوربین‌های فعلی ضمیمه شده است.

 


fixed4 frag(v2f i) : SV_Target
{
    float3 rayDirection;
    //این همون تابع بالایی هست که توضیحش دادیم
    float dist = GetDistanceFromDepth(i.uv.xy, rayDirection);
   
    // موقعیت دوربین(فضای جهان)
    float3 rayOrigin = _cameraPos;
    // محاسبه ی چرخش دوربین
    rayDirection = mul(_cameraMat, float4(rayDirection, 0.0)).xyz;

    //...
    //! بقیش در راهه

}

 

همان طور که می‌بینید تابع گرفتن فاصله از عمق را به رنگ بنفش هایلایت کردم. با این تابع فاصله را می‌گیریم و در متغیر اعشاری به نام dist ذخیره می‌کنیم. در نهایت ما یک float3  را به عنوان یک متغیر خروجی پاس دادیم، یعنی همان جهت اشعه (RayDirection). بنابراین از این تابع با FOV مناسب خارج می‌شود. با این وجود فاقد چرخش دوربین است.ما می‌توانیم موقعیت را با یک متغیر یکسان استاندارد (_cameraPos) به دست آوریم و RayDirection را در View matrix ضرب کنیم. به همین دلیل من از 0.0 به عنوان پارامتر w استفاده می‌کنم؛ زیرا ما نمی‌خواهیم موقعیت دوربین را در این متغیر ذخیره کنیم. ما فقط آن را چرخانده‌ایم.

تصویر زیر پیاده‌سازی کد بالا را نشان می‌دهد، همان طور که در تصویر زیر مشاهده می‌کنید، دو کره زرد و یک مکعب مقیاس‌پذیر وجود دارد. یکی از آن‌ها (در سمت راست) از چندضلعی(polygon) استفاده می‌کند. مکعب دقیقا همان طور که انتظار می‌رود، کره را تقسیم می‌کند. در سمت چپ، از تنظیمات ما برای محاسبه درست اطلاعات FOV و موقعیت و چرخش دوربین استفاده می‌کند.

همچنین توجه کنید که لبه‌های face مقابل مکعب چقدر صاف است. جایی که با کره تقسیم شده، با Raymarching رندر شده است. آن را با کره نسبتا high-poly در سمت راست مقایسه کنید.

رسم اشکال هندسی با استفاده از توابع فاصله

رندرینگ با Raymarching نیازمند درک جدی ریاضی یا یک Cheat واقعا خوب است. متاسفانه منابع زیادی وجود ندارد. دراین‌جا یک مقاله با توابع فاصله برای هر شکل قابل مشاهده است:

Inigo Quilez

بیایید به جای کره از شکل متفاوتی استفاده کنیم مانند دونات.

float sdTorus(float p, float t) : SV_Target
{
    float2 q= float2 (lengh (p.xz)-t.x,p.y); 
    return lengh (q)-t.y;

}

 

 این یک تابع فاصله‌ی شکل دونات است. اگر شما ناآشنا هستید، به پایین بروید تا یک دونات را ببینید. تابع فاصله، فاصله را از یک نقطه، به نزدیک‌ترین نقطه روی شکل اولیه (Primitive) بر می‌گرداند. این چیزی است که برای رندر کردن دونات‌مان استفاده می‌کنیم. در تصویر زیر، می‌توانید شکل‌های سیاه، دایره‌های قرمز، نقاط آبی و خطوط قرمز را مشاهده کنید. نقطه آبی در پایین سمت چپ دوربین است. نقطه آبی در بالا سمت راست نقطه‌ای است که به آن نگاه می‌شود. ما اطلاعاتی در مورد هر چیزی به جز فاصله تا نزدیک‌ترین سطح (خط سیاه در مرکز پایین) نداریم. بنابراین ما از این فاصله استفاده می‌کنیم تا به جلو حرکت و روند را تکرار کنیم، تا زمانی که به سطحی می‌رسیم که ما می‌خواهیم.

لینک راهنما

بیایید اول نگاهی به آن چه برای پیاده‌سازی نیاز داریم بیندازیم. در اینجا لیست TODO وجود دارد:

- دریافت مبدا پرتو (موقعیت دوربین)

- جهت گیری پرتو (دوربین FOV، نسبت ابعاد، و چرخش)

- اضافه کردن یک تابع فاصله کد اضافه (Torus)

- پرتاب کردن پرتو به سمت شکل

- دریافت فاصله از سطح شکل، در آن پرتو

آن چه ما برای اولین بارانجام خواهیم داد محاسبه یک نقطه است. برای این کار از معادله نقطه در امتداد یک بردار استفاده می‌کنیم تا فاصله‌ای خاص را در امتداد اشعه حرکت دهیم. سپس فاصله را از شکل اولیه(Primitive) بررسی می‌کنیم. ما فاصله‌ای را که در حال حرکت در امتداد اشعه بود، با استفاده از فاصله‌ای که محاسبه کردیم افزایش می‌دهیم . سپس ما روند را تکرار می‌کنیم. بنابراین این روند را در یک حلقه FOR کنترل می‌کنیم.

// اجازه دهید فاصله ای را که قصد داریم محاسبه کنیم را ذخیره کنیم
floatd=0.0f:
//ما در امتداد اشعه گام به گام پیش می رویم، 64 بار.این مقدرا می تواند عوض شود
for(inti=0;i<64;i++)
{
   //اینجا جایی است که ما یک موقعیت را در امتداد اشعه مان محاسبه می کنیم
   //اولین تکرار همانند ری اوریجین فقط خواهد بود
   float pos=rayOrigin + rayDirection * d;

   //این فاصله از نقطه مان به نزدیک ترین نقطه روی دونات می باشد
   float torusDistance=sdTorus (pos, float2(0.5,0.25));

   d+= torusDistance
}

//
//بقیه‌ش مونده

شاید وقت آن است که بعضی موارد را روشن کنیم؟

آنچه که ما در کد بالا انجام دادیم، تکرار حرکت در امتداد یک پرتو تا زمانی که فاصله ای برای کار کردن داشتیم است.اگر فقط حلقه را یک بار تکرار کنید، شما فقط اولین تخمین فاصله را بازمی­گردانید.به همین دلیل است که ما باید کد را تکرار کنیم. در حال حاضر، همه چیزهایی که رندر شده  زرد است. ما با موفقیت یک Torus ایجاد کردیم و شما می­توانید هر کدام از توابع فاصله را در اینجا مشاهده کنید تا ببینید چگونه کار می­کنند. در نهایت، ما چیزی کمی پیشرفته تر را پوشش خواهیم داد.

گرفتن اطلاعات سبک G-Buffer

برای استفاده از هر مدل نورپردازی، نیاز به اطلاعات بیشتری داریم. در حال حاضر، ما فقط فاصله­ای را در امتداد یک اشعه قرار داده­ایم و هیچ جزئیاتی برای دونات­مان در نظر نگرفتیم. برای کار کردن بیشتر روی شکل،  به موارد زیر نیاز داریم:

- مختصات سه بعدی (3D Coordinates)

- نرمال سطوح (Surface Normals)

خوشبختانه، هر دوی آن­‌ها بسیار آسان بدست می­‌آیند.

چگونه موقعیت و Normals را بدست آوریم:

 

//این به حد کافی گویاست ما فاصله ای داریم که
//برای گرفتن موقعیت جهانی نیاز دارد تا به سمت پایین حرکت کند
float pos=rayOrigin + rayDirection * d;

//در اینجا موقعیت را در محور های ایکس وای و زد جا به جا می کنیم
//و آن را برای براورد سطح نرمال آن را نرمالایز می کنیم
//اعلان متغییر زیر به ما اجازه میده که سحر و جادو کنیم
float2 eps= float3(0.0005, 0.0, 0.0 );<64;i++)


//این طوری شاید زشت به نظر برسد به خاطر همین می توانید آن را در یک تابع به نام مپ قرار دهید

//تعریف شکل دونات و سپس محاسبه ی نرمال سطح
#define TORUS(p)sdTorus(p,float2(0.5, 0.25)).x
float3 nor= float3
//این فاصله از نقطه مان به نزدیک ترین نقطه روی دونات می باشد
TORUS(pos+eps.xyy) - TORUS(pos-eps.xyy) ,
TORUS(pos+eps.yxy) - TORUS(pos-eps.yxy) ,
TORUS(pos+eps.yyx) - TORUS(pos-eps.yyx) ,
#undef TORUS

nor = normalize(nor);


//دلیل اینکه کار می کنه اینه که ما نتیجه را نرمالایز می کنیم
//اگر سطح بالا باشد اختلاف بین وای مثبت و وای منفی به طورنمونه بزرگتر از اختلاف
//بین ایکس مثبت یا منفی و وای مثبت یا منفی همه با هم جمع می شوند برای اینکه مقداری را براورد کنند
//معرکست شما دارید ری مرچینگ رو مشاهده می کنید که بر مبنای ریاضیه


//
//هنوز مونده

 به منظور روشن شدن آن، ما فقط از معادله نورپردازی استاندارد برای Phong که می توان در اینجا در ویکی پدیا پیدا کرد، استفاده می کنیم.

//چندتا متغییر برای شروع کارمون تعریف می کنیم
float3 l = normalize(sundir);
float3 e = normalize(rayOrigin);//در ری مرچینگ آی پوز همون ری اوریجینه
float3 r = normalize(-reflect(l,nor));

//محدوده ی امبینت
float3 ambient = 0.3;

//محدوده ی دیفیوز
float3 diffuse = max(dot(nor,l), 0.0);
diffuse = clamp(diffuse, 0.0, 1.0);

//اینم از اسپکولارمون
float3 specular = 0.04 * pow(max(dot(r,e),0.0),0.2);
specular = clamp(specular, 0.0, 1.0);
//الان همه ی افکت هایی که نوشتیم رو جمع می کنیم و روی دوناتمون اعمال می کنیم
float4 torusCol =float4(ambient + diffuse + specular, 1.0);

//
//تموم نمیشه که بازم هست

 خب تا این‌جا همه چیز خوب به نظر می‌رسد اما کار بافت دهی هنوز باقی مانده است. متاسفانه هیچ راهی برای گرفتن UV وجود ندارد. برای بافت دهی باید از Projection Mapping استفاده کنیم.

وقفه

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

Projection Mapping

بیایید دونات‌مان را تزئین کنیم. در اینجا بافت خمیری و در اینجا بافتی روی دونات که استفاده خواهیم کرد، وجود دارد. خب آخرین مرحله کدنویسی را انجام می‌دهیم.

پراجکشن مپینگ سه بعدی مسطح برای دونات‌مان

float4 frag(v2f) : SV_Target
{
//..
//هر چی تا حالا کد نوشتیم اینجا بنویسید
//..

//دونات را از دو پلین نمونه برداری کنید که ما از نرمال های ایکس و زد استفاده می کنیم
doughnutColor = tex2D(_Dough, pos.xy - float4(0.5, 0.5)).rgb * abs(nor.z);
doughnutColor += tex2D(_Dough, pos.zy - float4(0.5, 0.5)).rgb * abs(nor.x);

//از یک پلین بالا-به پایین برای نمونه برداری اسپرینکل (همون تکسچری که لینکش هست) استفاده کنید
//خب حالا باید دورشو ببریم یعنی کات آف داشته باشیم به خاطر همین از یک ایف الز استیت منت استفاده می کنیم
//یه نویز هم چاشنیش می کنیم که جالب تر به نظر برسه
float3 noiseOffset = tex2D(_Noise, pos.xz * 0.2).x * 0.5f;
if (nor.y + noiseOffset > 0.7)
{
doughnutColor = tex2D(_Sprinkles, pos.xz).rgb;
} else {
doughnutColor += float3(1.0, 0.75, 0.5); //رنگ اینجا باید کار کنه
}

torusCol.rgb *= doughnutColor;

//و در آخر یادتون باشه که ما چطور از بافر عمق فاصله را محاسبه کردیم
//شما اینجا می توانید بافر عمق دلخواهتون رو اینجا تست کنید

return (dist < d ? tex2D(_MainTex, uv) : torusCol);
}

 

نتیجه

در پست‌های بعدی، رندر حجمی را پیگیری خواهم کرد که در حال حاضر بیشترین پتانسیل را برای برای اثرات دود، شبیه سازی ذرات عظیم و ابرها دارد. دلیل آن این است که یونیتی Raymarching را پوشش می‌دهد. هر چیزی که ماهیت حجم دارد، می‌تواند با تکنیک‌های بسیار مشابه به آنچه که در بالا توضیح داده شد، رندر شود.

منبع: Gamasutra

دیدگاه‌ها