
مرحبا بالجميع!
تقوم شركتنا بتطوير ألعاب عبر الإنترنت ، ونحن الآن نعمل على إصدار محمول لمشروعنا الرئيسي. في هذه المقالة ، نريد مشاركة تجربة تطوير تظليل GLSL لمشروع Android مع الأمثلة
والمصادر .
عن المشروع
في البداية ، كانت اللعبة تعتمد على المتصفح باستخدام Flash ، ولكن الأخبار حول التوقف الوشيك لدعم Flash أجبرتنا على نقل المشروع إلى HTML5. تم استخدام Kotlin كلغة التطوير ، وبعد ستة أشهر تمكنا من إطلاق المشروع على Android. لسوء الحظ ، بدون التحسين على الأجهزة المحمولة ، كانت اللعبة تفتقر إلى الأداء.
لزيادة FPS ، تقرر إعادة تصميم محرك الرسومات. اعتدنا على استخدام عدة تظليل عالمي ، ولكن لكل تأثير قررنا الآن كتابة تظليل منفصل ، شحذ لمهمة محددة ، حتى نتمكن من جعل عملهم أكثر كفاءة.
ما افتقرنا إليه
يمكن تخزين Shaders في سلسلة ، ولكن هذه الطريقة تقضي على فحص بناء الجملة ومطابقة النوع ، لذلك يتم تخزين shaders عادةً في Assets أو ملفات Raw ، حيث يتيح لك هذا تمكين التحقق من الصحة عن طريق تثبيت المكون الإضافي لـ Android Studio. ولكن هذا النهج له أيضًا عيب - عدم إعادة الاستخدام: لإجراء تعديلات صغيرة ، عليك إنشاء ملف تظليل جديد.
بحيث:
- تطوير تظليل على Kotlin ،
- إجراء فحص بناء الجملة في مرحلة التجميع ،
- تكون قادرة على إعادة استخدام الرمز بين تظليل ،
كنت بحاجة لكتابة "محول" Kotlin إلى GLSL.
النتيجة المرجوة: توصف شفرة التظليل بأنها فئة Kotlin ، حيث تكون السمات ، والتفاوتات ، والزي الرسمي هي خصائص هذه الفئة. يتم استخدام معلمات المنشئ الأساسي للفئة للفروع الثابتة وتسمح لك بإعادة استخدام ما تبقى من كود التظليل. كتلة التهيئة هي جسم التظليل.
الحل
من أجل التنفيذ ، تم استخدام
مندوبي Kotlin. سمحوا لوقت التشغيل بمعرفة اسم الملكية المفوضة ، والحصول على لحظات المكالمات وتعيينها وإخطارهم بـ ShaderBuilder - الفئة الأساسية لجميع التظليل.
class ShaderBuilder { val uniforms = HashSet<String>() val attributes = HashSet<String>() val varyings = HashSet<String>() val instructions = ArrayList<Instruction>() ... fun getSource(): String = ... }
تنفيذ مندوبمندوب متغير:
class VaryingDelegate<T : Variable>(private val factory: (ShaderBuilder) -> T) { private lateinit var v: T operator fun provideDelegate(ref: ShaderBuilder, p: KProperty<*>): VaryingDelegate<T> { v = factory(ref) v.value = p.name return this } operator fun getValue(thisRef: ShaderBuilder, property: KProperty<*>): T { thisRef.varyings.add("${v.typeName} ${property.name}") return v } operator fun setValue(thisRef: ShaderBuilder, property: KProperty<*>, value: T) { thisRef.varyings.add("${v.typeName} ${property.name}") thisRef.instructions.add(Instruction.assign(property.name, value.value)) } }
تنفيذ باقي المندوبين على
جيثب .
مثال شادر:
// useAlphaTest , // , , , // . class FragmentShader(useAlphaTest: Boolean) : ShaderBuilder() { private val alphaTestThreshold by uniform(::GLFloat) private val texture by uniform(::Sampler2D) private val uv by varying(::Vec2) init { var color by vec4() color = texture2D(texture, uv) // static branching if (useAlphaTest) { // dynamic branching If(color.w lt alphaTestThreshold) { discard() } } // ShaderBuilder. gl_FragColor = color } }
وهنا مصدر GLSL الناتج (نتيجة FragmentShader (useAlphaTest = true) .getSource ()). يتم الحفاظ على محتوى وهيكل التعليمات البرمجية:
uniform sampler2D texture; uniform float alphaTestThreshold; varying vec2 uv; void main(void) { vec4 color; color = texture2D(texture, uv); if ((color.w < alphaTestThreshold)) { discard; } gl_FragColor = color; }
من المناسب إعادة استخدام شفرة التظليل عن طريق تعيين معلمات مختلفة عند تجميع المصدر ، ولكن هذا لا يحل مشكلة إعادة الاستخدام تمامًا. في حالة الحاجة إلى كتابة نفس الرمز في تظليل مختلفة ، يمكنك وضع هذه التعليمات في ShaderBuilderComponent منفصل وإضافتها حسب الضرورة إلى ShaderBuilders الرئيسي:
class ShadowReceiveComponent : ShaderBuilderComponent() { … fun vertex(parent: ShaderBuilder, inp: Vec4) { vShadowCoord = shadowMVP * inp ... parent.appendComponent(this) } fun fragment(parent: ShaderBuilder, brightness: GLFloat) { var pixel by float() pixel = texture2D(shadowTexture, vShadowCoord.xy).x ... parent.appendComponent(this) } }
مرحى ، تسمح لك الوظائف المستلمة بكتابة تظليل على Kotlin ، وإعادة استخدام الرمز ، والتحقق من بناء الجملة!
الآن ، دعونا نتذكر
Swizzling في GLSL وننظر في تنفيذها في Vec2 ، Vec3 ، Vec4.
class Vec2 { var x by ComponentDelegate(::GLFloat) var y by ComponentDelegate(::GLFloat) } class Vec3 { var x by ComponentDelegate(::GLFloat) ... // 9 Vec2 var xx by ComponentDelegate(::Vec2) var xy by ComponentDelegate(::Vec2) ... } class Vec4 { var x by ComponentDelegate(::GLFloat) ... // 16 Vec2 var xy by ComponentDelegate(::Vec2) ... // 64 Vec3 var xxx by ComponentDelegate(::Vec3) ... }
في مشروعنا ، يمكن أن يحدث تجميع التظليل في دورة اللعبة عند الطلب ، وتؤدي عمليات تخصيص الكائنات هذه إلى إنشاء مكالمات GC رئيسية ، وتظهر التأخيرات. لذلك ، قررنا نقل تجميع مصادر التظليل إلى مرحلة التجميع باستخدام معالج التعليقات التوضيحية.
نحتفل بالصف مع شرح ShaderProgram:
@ShaderProgram(VertexShader::class, FragmentShader::class) class ShaderProgramName(alphaTest: Boolean)
ويجمع معالج التعليقات التوضيحية جميع أنواع التظليل اعتمادًا على معلمات منشئي فئات الرأس والجزء بالنسبة لنا:
class ShaderProgramNameSources { enum class Sources(vertex: String, fragment: String): ShaderProgramSources { Source0("<vertex code>", "<fragment code>") ... } fun get(alphaTest: Boolean) { if (alphaTest) return Source0 else return Source1 } }
الآن يمكنك الحصول على نص التظليل من الفصل الذي تم إنشاؤه:
val sources = ShaderProgramNameSources.get(replaceAlpha = true) println(sources.vertex) println(sources.fragment)
نظرًا لأن نتيجة دالة get - ShaderProgramSources هي القيمة من التعداد ، فمن المناسب استخدامها كمفاتيح في تسجيل البرنامج (ShaderProgramSources) -> CompiledShaderProgram.
يحتوي
GitHub على شفرة المصدر للمشروع ، بما في ذلك معالج التعليقات التوضيحية وأمثلة بسيطة للتظليل والمكونات.