النمذجة الصلبة هي عملية دمج الأجزاء معًا بطرق فريدة لتشكيل أشكال أكثر تعقيدًا. يتضمن ذلك العمليات البولينية الاتحاد، الطرح، والتقاطع، المعروفة عمومًا باسم الهندسة الصلبة التفاعلية (CSG). يمكنك إجراء النمذجة الصلبة في أي مكان: في الاستوديو، المكونات الإضافية، وحتى داخل اللعبة على كل من الخادم والعميل.
بالإضافة إلى CSG البوليني، تدعم النمذجة الصلبة أيضًا الشبكات، طالما أنها محكمة، وعمليات مثل السحب والتفكيك التي تتيح لك وللاعبيك تقطيع، قص، وتفتيت الهندسة لتفاعلات لعب فريدة.


الهندسة المحكمة
هناك ثلاثة عناصر أساسية في الشبكات:
- الرأس - نقطة واحدة على الشبكة.
- الحافة - خط يربط وصلتين.
- الوجه - مساحة سطح بين ثلاث رؤوس أو أكثر.



يمكن أن تعمل عمليات النمذجة الصلبة فقط مع الهندسة المحكمة؛ في الواقع، فإن "صلب" و"محكم" هما مترادفان. من الناحية الفنية، تعني أن الشبكة المحكمة مغلقة، وقابلة للمعالجة، وغير متداخلة ذاتيًا. لهذه المصطلحات تعريفات صارمة، لكن إليك بعض القواعد البسيطة:
- يجب أن يكون لكل وجه جانب "داخلي" وآخر "خارجي". يتم تحديد ذلك بواسطة ترتيب توجيه الوجه، وهو ترتيب رؤوسه الثلاثة.
- يجب أن تشارك كل حافة بالضبط مثلثين. هذا يعني أنه لا يمكن أن تكون هنالك ثقوب في الشبكة لأن الحواف الموجودة حول حواف الثقب سيكون لها مثلث واحد فقط.
- لا يمكن أن تمُر الوجوه عبر الوجوه الأخرى.
- يجب أن توافق المثلثات المجاورة على أي جانب هو الجانب "الخارجي".
- يجب أن يحتوي كل رأس على مروحة واحدة من المثلثات المجاورة بالضبط.

نظام النمذجة الصلبة قادر على إصلاح مشاكل صغيرة محددة بالشبكة تلقائيًا، ولكن بشكل عام، ستفشل استدعاءات API إذا كانت الشبكة غير المحكمة. ليس هناك طريقة واحدة تناسب الجميع لإصلاح شبكة غير محكمة موجودة، لكن هناك العديد من المكونات الإضافية في Blender يمكن أن تساعد، مثل 3D Print Toolbox وMesh Repair Tools. كخيار آخر، يحتوي Meshlab أيضًا على أدوات مفيدة جدًا مدمجة لمحاولة جعل الشبكة قابلة للمعالجة، وهو المتطلب الرئيسي للشبكة لتكون محكمة.
طريقة واحدة لرؤية أن الشبكة سيكون من الصعب للغاية جعلها محكمة هي النظر إليها في الاستوديو من جميع الزوايا، ثم محاولة تمكين وتعطيل خاصية MeshPart.DoubleSided الخاصة بالشبكة. إذا كنت تستطيع رؤية أي اختلاف، فإن الشبكة هي مجرد قشرة والأدوات المذكورة أعلاه لن تعمل لأنها لا يمكن أن تخمن ما هو الفضاء داخل مقابل خارج الشبكة. ومع ذلك، إذا كنت تريد شبكة رقيقة فقط وليس من المهم الحفاظ على أبعاد الشبكة كما هي، يمكنك استخدام Solidify modifier في Blender لتكثيف القشرة قليلاً إلى شبكة محكمة.

النمذجة الصلبة في الاستوديو
يمكنك إجراء ثلاث عمليات بولينية أساسية باستخدام أربعة أدوات داخل شريط أدوات النموذج.

| الأداة | اختصار | وصف |
|---|---|---|
| الـاتحاد | Shift Ctrl G (Windows) Shift ⌘ G (Mac) | دمج جزئين أو أكثر معًا لتشكيل اتحاد صلب واحد. |
| التقاطع | Shift Ctrl I (Windows) Shift ⌘ I (Mac) | التقاطع بين الأجزاء المتداخلة لتكوين تقاطع صلب واحد. |
| الطرح | Shift Ctrl N (Windows) Shift ⌘ N (Mac) | طرح الأجزاء، مفيد لصنع الثقوب والتجاويف. |
| فصل | Shift Ctrl U (Windows) Shift ⌘ U (Mac) | فصل الاتحاد أو التقاطع مرة أخرى إلى أجزائه الفردية. |
اتحاد الأجزاء
تجمع أداة الاتحاد بين جزئين أو أكثر معًا لتشكيل UnionOperation صلبة واحدة.


لدمج الأجزاء معًا في اتحاد:
- حدد جميع الأجزاء التي تريد دمجها.
تقاطع الأجزاء
تقوم أداة التقاطع بتقاطع الأجزاء المتداخلة لتشكيل IntersectOperation صلبة واحدة.


للتقاطع بين الأجزاء المتداخلة معًا:
- حدد جميع الأجزاء التي تريد التقاطع معها.
طرح الأجزاء
تعمل أداة الطرح على طرح جزء بحيث عند دمجه مع جزء آخر، يتم طرح شكل الجزء المطرود من الجزء الآخر.


لطرح جزء من الأجزاء المتداخلة الأخرى:
- حدد الجزء الذي ترغب في طرحه من الأجزاء الأخرى.
- انقر على الطرح. يصبح الجزء مُعَلمًا كجزء مطروح ويظهر رمز مطروح في المتصفح. يصبح الجزء شبه شفاف مع صبغة حمراء للإشارة إلى حالته.
- حدد كل من الجزء المطروح والأجزاء التي تريد طرحها منها.
- انقر على الاتحاد. يتم قطع الجزء المائل من الأجزاء المتضمنة المتداخلة.
تم الكشف عن العلامة للبرمجة، لذا يمكنك أيضًا طرح الأجزاء عن طريق إضافة العلامة rbxNegate من سكريبت أو مكون إضافي. لم يعد يستخدم NegateOperation.
فصل الاتحاد أو التقاطعات
تقوم أداة الفصل بفصل UnionOperation مرة أخرى إلى أجزائها الفردية، مما يعمل فعليًا كأداة "تراجع" للاتحادات والتقاطعات.
لفصل اتحاد أو تقاطع مرة أخرى إلى أجزاء فردية:
- حدد الاتحاد أو التقاطع.
- انقر على فصل. تعود الأجزاء إلى شكلها الأصلي.
النمذجة الصلبة داخل اللعبة
يمكنك أيضًا إجراء عمليات النمذجة الصلبة أثناء تشغيل اللعبة باستخدام وظائف GeometryService.
UnionAsync()، IntersectAsync()، و SubtractAsync()
بالمثل مثل أدوات العمليات البولينية الأساسية المدمجة في الاستوديو، يمكنك استخدام وظائف GeometryService مثل UnionAsync()، IntersectAsync()، و SubtractAsync() لإجراء عمليات بولينية أساسية أثناء تشغيل اللعبة. على سبيل المثال، يستخدم السكريبت التالي SubtractAsync() لطرح حجم جزء من جزء آخر.
local GeometryService = game:GetService("GeometryService")
local mainPart = Instance.new("Part")
local otherPart = Instance.new("Part")
otherPart.Position = Vector3.new(1, 0.5, 1)
local success, newParts = pcall(function()
return GeometryService:SubtractAsync(mainPart, {otherPart})
end)
if success and newParts then
for _, newPart in pairs(newParts) do
newPart.Parent = workspace
end
end

لتوضيح أكثر، يجمع المثال البرمجي التالي بين هندسة mainPart والأجزاء في مصفوفة otherParts، ثم يدمر الأجزاء الأصلية المعنية في العملية. يمكنك استبدال استدعاء UnionAsync() بـ IntersectAsync() أو SubtractAsync() لإجراء العمليات البولينية الأخرى.
local GeometryService = game:GetService("GeometryService")
local mainPart = workspace.BlueBlock
local otherParts = { workspace.PurpleCylinder }
local options = {
CollisionFidelity = Enum.CollisionFidelity.Default,
RenderFidelity = Enum.RenderFidelity.Automatic,
SplitApart = false,
}
-- نفذت عملية الاتحاد في pcall() لأنها غير متزامنة
local success, newParts = pcall(function()
return GeometryService:UnionAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- ضع إرشادات لعملية إعادة تخصيص الأجزاء الناتجة
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- دمر الأجزاء الأصلية
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end
طالما أن جميع المدخلات تتكون من أجزاء بدائية بدلاً من MeshParts، فإن العمليات البولينية للاتحاد، التقاطع، والطرح ستنتج PartOperation مع قطعتين من البيانات المخزنة: شجرة CSG تُعرف باسم CSGTree، وشبكة للرندر.
بالمقارنة مع BasePart:UnionAsync()/BasePart:IntersectAsync()/BasePart:SubtractAsync()، تختلف وظائف GeometryService البولينية كما يلي:
- المخرجات هي مصفوفة من النماذج بدلاً من نموذج واحد.
- لا تحتاج الأجزاء المدخلة إلى أن تكون متصلة بالمشهد، مما يسمح بعمليات خلفية.
- عند تعيين خيار SplitApart على true (الافتراضي)، سيعود كل جسم متميز في PartOperation/MeshPart الخاصة به.
- جميع الأجزاء التي يتم إرجاعها في فضاء الإحداثيات للجزء الرئيسي، لذا فإن مواقع PVInstance.Origin لها هي نفسها مثل الجزء الرئيسي. هذا يبقي رؤوس الشبكة في نفس الموضع بالنسبة للجسم كما كان قبل العملية، ولكن هذا يعني أيضًا أن (0, 0, 0) لجزء المرتجع ليس بالضرورة في مركز جسده.
SweepPartAsync()
تخلق الدالة GeometryService:SweepPartAsync() جزءًا من MeshPart الذي له شكل الجزء المدخل المسحوب عبر مجموعة معينة من المواقع CFrame. يمكن أن تكون هذه الوظيفة مفيدة جدًا لأداء عمليات التقطيع والقطع.
يمكن أن يكون المدخل Part، PartOperation، أو MeshPart. يتم تعريف شكل النتيجة كاتحاد للأصداف المحدبة لكل زوج متجاور من CFrames؛ إذا تم تقديم CFrame واحد فقط، ستكون النتيجة قذيفة محدبة للجزء المدخل.
لتوضيح كيف تعمل هذه الوظيفة، يقوم المثال البرمجي التالي بسحب كرة عبر مجموعة من المواقع CFrame لإنشاء شكل حلزوني:
local GeometryService = game:GetService("GeometryService")
local inputPart = Instance.new("Part")
inputPart.Shape = Enum.PartType.Ball
local cframeList = {}
for i = 1, 50 do
local rotation = CFrame.Angles(0, i * 0.5, 0)
local position = Vector3.new(0, i * 0.1, -1)
table.insert(cframeList, rotation * CFrame.new(position))
end
local success, sweptPart = pcall( function()
return GeometryService:SweepPartAsync(inputPart, cframeList)
end)
if success and sweptPart then
sweptPart.Parent = workspace
end

مثال

تستخدم هذه الأمثلة GeometryService:SweepPartAsync() لتحقيق ميزة تقطيع بسيف أو مسدس ليزر، حيث يعتمد حركة السيف على موضع الماوس الخاص باللاعب. يتم تسجيل حركة الماوس الخاصة بالمستخدم كقائمة من CFrames، ثم يقوم SweepPartAsync() بإنشاء شبكة قطع من هذه البيانات، ثم يتم طرح شبكة القطع من الجزء الذي تم ضربه.
لجعل هذا المثال يعمل في الاستوديو:
أنشئ Script التالي في ServerScriptService لأداء جميع عمليات النمذجة الصلبة.
local ReplicatedStorage = game:GetService("ReplicatedStorage")local GeometryService = game:GetService("GeometryService")local DrawCurveEvent = ReplicatedStorage:WaitForChild("DrawCurveEvent")DrawCurveEvent.OnServerEvent:Connect(function(player, cframeList, hitInstance)local blade = Instance.new("Part")blade.Size = Vector3.new(0.2, 0.2, 15.0)local success, sweptPart = pcall( function()return GeometryService:SweepPartAsync(blade, cframeList)end)if success and sweptPart then-- تصور السحبsweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- طرح السحب من الكائن الضاربlocal subtractSuccess, newParts = pcall( function()return GeometryService:SubtractAsync(hitInstance, {sweptPart})end)if subtractSuccess and newParts thenfor _, newPart in pairs(newParts) donewPart.Parent = hitInstance.ParentnewPart.Anchored = trueendhitInstance:Destroy()endendend)أنشئ LocalScript التالي في StarterPlayerScripts لمعالجة إدخال المستخدم.
local ReplicatedStorage = game:GetService("ReplicatedStorage")local GeometryService = game:GetService("GeometryService")local DrawCurveEvent = ReplicatedStorage:WaitForChild("DrawCurveEvent")DrawCurveEvent.OnServerEvent:Connect(function(player, cframeList, hitInstance)local blade = Instance.new("Part")blade.Size = Vector3.new(0.2, 0.2, 15.0)local success, sweptPart = pcall( function()return GeometryService:SweepPartAsync(blade, cframeList)end)if success and sweptPart then-- تصور السحبsweptPart.Parent = workspacesweptPart.Transparency = 0.5sweptPart.Anchored = truesweptPart.CanQuery = false-- طرح السحب من الكائن الضاربlocal subtractSuccess, newParts = pcall( function()return GeometryService:SubtractAsync(hitInstance, {sweptPart})end)if subtractSuccess and newParts thenfor _, newPart in pairs(newParts) donewPart.Parent = hitInstance.ParentnewPart.Anchored = trueendhitInstance:Destroy()endendend)أنشئ RemoteEvent في ReplicatedStorage يسمى DrawCurveEvent.
FragmentAsync()
تتيح لك دوال GeometryService:FragmentAsync() و GeometryService:GenerateFragmentSites() تفتيت جزء إلى قطع بأشكال طبيعية. تستخدم GeometryService:FragmentAsync() تفكيك فورونوي لتقسيم جزء واحد إلى عدة نماذج من MeshPart وفقًا لنمط النقاط التي تم تمريرها، بينما تعتبر GeometryService:GenerateFragmentSites() دالة مساعدة تقوم بإنشاء نقاط تعرف باسم مواقع فورونوي لتمريرها إلى FragmentAsync().
لتوضيح كيف تعمل هذه الوظائف معًا، يقوم المثال البرمجي التالي بإنشاء مواقع فورونوي لتفتيت جزء كتلة:
local GeometryService = game:GetService("GeometryService")
local inputPart = Instance.new("Part")
inputPart.Position = Vector3.new(0, 0.7, 20)
local sites = GeometryService:GenerateFragmentSites(inputPart)
local success, fragments = pcall( function()
return GeometryService:FragmentAsync(inputPart, sites)
end)
if success and fragments then
for _, item in fragments do
local instance = item.Instance
instance.Parent = workspace
end
end

أمثلة

يقوم السكريبت التالي بتفتيت منطقة من جزء، المحددة بموقع ونصف القطر. قد يأتي الموقع عادةً من تصادم مادي أو من شعاع من اللاعب.
يكون العنصر الأول من مصفوفة المواقع التي تولدها GenerateFragmentSites() مصفوفة داخلية تحتوي على جميع المواقع التي تقع خارج نصف القطر المطلوب. إذا كنت ترغب في القيام بشيء محدد بالجزء الباقي "غير المتفكك" من الكائن، يمكنك العثور على هذه الجزء من خلال التحقق من fragments[i].Index == 1 أثناء الدوران عبر نتائج FragmentAsync().
local GeometryService = game:GetService("GeometryService")
function fragmentAtPosition(player, part, contactPoint, radius)
local allSites = GeometryService:GenerateFragmentSites(part, {Origin = contactPoint, Radius = radius})
local success, fragments = pcall( function()
return GeometryService:FragmentAsync(part, allSites)
end)
if not success then
warn("فشل في التفتيت:"..tostring(fragments))
return
end
local decals = {}
for _,child in pairs(part:GetChildren()) do
if child:IsA("Decal") or child:IsA("SurfaceAppearance") then
table.insert(decals,child)
end
end
for i = 1, #fragments do
local fragment = fragments[i].Instance
if fragment == nil then
continue
end
for _,d in pairs(decals) do
local d2 = d:Clone()
d2.Parent = fragment
end
fragment.Anchored = false
fragment.Parent = part.Parent
fragment:SetNetworkOwner(player)
end
if #fragments ~= 0 then
part:Destroy()
end
return fragments
end
يقوم السكريبت التالي بتفتيت القطع على الجزء الأول داخل شكل محدد باستخدام الجزء الثاني كقالب. ستؤدي فقط مواقع فورونوي داخل الجزء الثاني إلى نتائج مختلفة. ستدمج جميع المواقع الأخرى خلاياها في جزء واحد.


local GeometryService = game:GetService("GeometryService")
function fragmentWithinStencil(player, part)
local overlapParams = OverlapParams.new()
overlapParams.FilterType = Enum.RaycastFilterType.Include
overlapParams.FilterDescendantsInstances = {workspace.Stencil}
overlapParams.RespectCanCollide = false
local sensor = Instance.new("Part")
sensor.Size = Vector3.new(0.01, 0.01, 0.01)
local allSites = GeometryService:GenerateFragmentSites(part, {SiteSpacing = 0.9})
local fragmentSites = {}
local mainPartSites = {}
for _, site in ipairs(allSites) do
sensor.CFrame = CFrame.new(site)
local partsFound = workspace:GetPartsInPart(sensor, overlapParams)
if #partsFound > 0 then
table.insert(fragmentSites, site)
else
table.insert(mainPartSites, site)
end
end
local sortedSites = fragmentSites
table.insert(sortedSites, mainPartSites)
workspace.Stencil:Destroy()
local success, fragments = pcall( function()
return GeometryService:FragmentAsync(part, sortedSites, {SplitApart = false})
end)
if not success then
warn("فشل في التفتيت:"..tostring(fragments))
return
end
local decals = {}
for _,child in pairs(part:GetChildren()) do
if child:IsA("Decal") or child:IsA("SurfaceAppearance") then
table.insert(decals,child)
end
end
for i = 1, #fragments do
local fragment = fragments[i].Instance
if fragment == nil then
continue
end
for _,d in pairs(decals) do
local d2 = d:Clone()
d2.Parent = fragment
end
fragment.Anchored = false
fragment.Parent = part.Parent
fragment:SetNetworkOwner(player)
end
if #fragments ~= 0 then
part:Destroy()
end
return fragments
end
السكريبت التالي هو حالة استخدام أضيق بكثير، لكنه يوضح قوة بيانات الفهرسة التي تعود من GeometryService:FragmentAsync().
على سبيل المثال، تحتوي العديد من الأماكن على مبانٍ تشكلت من أجزاء كتلة غير موحدة. إذا أصابت قنبلة، قذيفة مدفع، أو مطرقة كبيرة، فستريد تفتيت جميع جدران الأجزاء. يقوم هذا السكريبت بتفتيت جميع الأجزاء المجاورة، ثم يوحد الشظايا من أجزاء مختلفة سويًا لإخفاء الفواصل تمامًا.
يتضمن ذلك عمليات Async متعددة، لذا قد لا يكون مناسبًا للاستخدام في اللعبة كاستجابة فورية لإدخال المستخدم، مثل أداة المطرقة الكبيرة.


local GeometryService = game:GetService("GeometryService")
function fragmentCrossPart(player, part, contactPoint, radius)
local allSites = GeometryService:GenerateFragmentSites(part, {Origin = contactPoint, Radius = radius})
local fragmentsSorted = {}
for i = 1, #allSites do
fragmentsSorted[i] = {}
end
local partsFound = workspace:GetPartBoundsInRadius(contactPoint, radius)
for i, part in ipairs(partsFound) do
local success, fragments = pcall( function()
return GeometryService:FragmentAsync(part, allSites)
end)
if not success then
warn("فشل في التفتيت:"..tostring(fragments))
return
end
for i = 1, #fragments do
local fragment = fragments[i].Instance
local siteIndex = fragments[i].Index
if fragment == nil or siteIndex == nil then
continue
end
table.insert(fragmentsSorted[siteIndex], fragment)
end
end
for i = 1, #fragmentsSorted do
local fragmentList = fragmentsSorted[i]
if #fragmentList == 0 then
continue
end
if #fragmentList == 1 then
local fragment = fragmentList[1]
fragment.Anchored = false
fragment.Parent = part.Parent
fragment:SetNetworkOwner(player)
continue
end
if i == #allSites then
for j = 1, #fragmentList do
local fragment = fragmentList[j]
fragment.Parent = part.Parent
fragment.Anchored = true
end
continue
end
local mainPart = fragmentList[1]
local otherParts = {}
for j = 2, #fragmentList do
table.insert(otherParts, fragmentList[j])
end
local success, results = pcall( function()
return GeometryService:UnionAsync(mainPart, otherParts)
end)
if not success then
warn("فشل في الاتحاد:"..tostring(results))
return
end
for j = 1, #results do
results[j].Parent = part.Parent
results[j].Anchored = false
end
end
for i, part in ipairs(partsFound) do
part:Destroy()
end
end
السكريبت التالي هو بديل Luau مطابق تقريبًا لـ GeometryService:GenerateFragmentSites(). إذا كنت تريد سلوكًا مشابهًا لـ GeometryService:GenerateFragmentSites() ولكن ترغب في إجراء تغييرات طفيفة، يمكنك استخدام هذا كنقطة انطلاق.
يستخدم شبكة نقاط فوضوية، ويضمن أن المنطقة المفرقة متناسقة، على عكس النقاط العشوائية بالكامل.
local function generateFragmentSites(part: BasePart, siteSpacing: number?, origin: Vector3?, radius: number?): {Vector3}
local RANDOMNESS_MULTIPLIER = 1.0 -- استخدم هذا لتعديل مقدار الفوضى
if (origin and not radius) or (radius and not origin) then
warn("يجب تقديم كل من الأصل ونصف القطر، أو لا شيء.")
return {}
end
local isLocalized = (radius ~= nil) -- المُحلي يعني عدم تفتيت الكل، فقط جزء.
local partCFrame = part.ExtentsCFrame
local gridDimensions: Vector3
local localGridCenter: Vector3
local spacing
if siteSpacing then
spacing = siteSpacing
elseif isLocalized then
spacing = radius * 0.5
else
local partSize = part.Size
local volume = partSize.X * partSize.Y * partSize.Z
spacing = (volume / 5) ^ (1/3)
end
if isLocalized then
local localOrigin = partCFrame:PointToObjectSpace(origin)
local gridSize = math.ceil(radius * 2 / spacing) + 3
gridDimensions = Vector3.new(gridSize, gridSize, gridSize)
localGridCenter = localOrigin
else
local partSize: Vector3 = part.Size
local xCount = math.ceil(partSize.X / spacing)
local yCount = math.ceil(partSize.Y / spacing)
local zCount = math.ceil(partSize.Z / spacing)
gridDimensions = Vector3.new(xCount, yCount, zCount)
localGridCenter = Vector3.zero
end
local totalGridSize = gridDimensions * spacing
local halfCell = Vector3.new(spacing, spacing, spacing) * 0.5
local localStartOffset = localGridCenter - (totalGridSize * 0.5) + halfCell
local innerJitter = spacing * 0.5 * RANDOMNESS_MULTIPLIER
local outerJitter = math.min(spacing * 0.5 * 0.866, innerJitter)
local sitesFlatList = {}
for x = 0, gridDimensions.X - 1 do
for y = 0, gridDimensions.Y - 1 do
for z = 0, gridDimensions.Z - 1 do
local isOuterShell =
x == 0 or x == gridDimensions.X - 1 or
y == 0 or y == gridDimensions.Y - 1 or
z == 0 or z == gridDimensions.Z - 1
local jitterAmount = if (isOuterShell and isLocalized) then outerJitter else innerJitter
local jitterOffset = Vector3.new(
(math.random() - 0.5) * 2 * jitterAmount,
(math.random() - 0.5) * 2 * jitterAmount,
(math.random() - 0.5) * 2 * jitterAmount
)
local offsetInGrid = Vector3.new(x, y, z) * spacing
table.insert(sitesFlatList, localStartOffset + offsetInGrid + jitterOffset)
end
end
end
local sitesListFinal = {}
if isLocalized then
local mainPartSites = {}
for _, localSite in ipairs(sitesFlatList) do
local worldSite = partCFrame * localSite
local distance = (worldSite - origin).Magnitude
if distance < radius then
table.insert(sitesListFinal, worldSite)
else
table.insert(mainPartSites, worldSite)
end
end
table.insert(sitesListFinal, 1, mainPartSites)
else
for _, localSite in ipairs(sitesFlatList) do
local worldSite = partCFrame * localSite
table.insert(sitesListFinal, worldSite)
end
end
return sitesListFinal
end
الحفاظ على القيود
إذا كان للجزء المدخل قيود أو ملحقات ترغب في الاحتفاظ بها، يمكنك نقلها إلى الأجزاء الناتجة. قد يكون من المرهق معرفة أي جزء ناتج ينبغي إرفاق قيود به، لذا يوصى باستخدام GeometryService:CalculateConstraintsToPreserve() لإنشاء جدول من التوصيات التي يمكنك المرور عليها وتطبيقها.
لتوضيح، يقوم المثال البرمجي التالي بإجراء عملية طرح، ويمر عبر الأجزاء الناتجة لإعادة تخصيص الأجزاء الناتجة وإعادة وضعها، ثم يحسب جدولًا من القيود والملحقات إما للحفاظ عليها أو إسقاطها قبل تدمير جميع الأجزاء الأصلية.
local GeometryService = game:GetService("GeometryService")
local mainPart = workspace.PurpleBlock
local otherParts = { workspace.BlueBlock }
local options = {
CollisionFidelity = Enum.CollisionFidelity.Default,
RenderFidelity = Enum.RenderFidelity.Automatic,
SplitApart = true,
}
local constraintOptions = {
tolerance = 0.1,
weldConstraintPreserve = Enum.WeldConstraintPreserve.All,
dropAttachmentsWithoutConstraints = false,
}
-- نفذت عملية الطرح في pcall() لأنها غير متزامنة
local success, newParts = pcall(function()
return GeometryService:SubtractAsync(mainPart, otherParts, options)
end)
if success and newParts then
-- ضع إرشادات لعملية إعادة تخصيص الأجزاء الناتجة
for _, newPart in pairs(newParts) do
newPart.Parent = mainPart.Parent
newPart.CFrame = mainPart.CFrame
newPart.Anchored = mainPart.Anchored
end
-- احسب القيود/الملحقات التي ينبغي الحفاظ عليها أو إسقاطها
local recommendedTable = GeometryService:CalculateConstraintsToPreserve(mainPart, newParts, constraintOptions)
-- الحفاظ على القيود/الملحقات بناءً على الجدول الموصى به
for _, item in pairs(recommendedTable) do
if item.Attachment then
item.Attachment.Parent = item.AttachmentParent
if item.Constraint then
item.Constraint.Parent = item.ConstraintParent
end
elseif item.NoCollisionConstraint then
local newNoCollision = Instance.new("NoCollisionConstraint")
newNoCollision.Part0 = item.NoCollisionPart0
newNoCollision.Part1 = item.NoCollisionPart1
newNoCollision.Parent = item.NoCollisionParent
elseif item.WeldConstraint then
local newWeldConstraint = Instance.new("WeldConstraint")
newWeldConstraint.Part0 = item.WeldConstraintPart0
newWeldConstraint.Part1 = item.WeldConstraintPart1
newWeldConstraint.Parent = item.WeldConstraintParent
end
end
-- دمر الأجزاء الأصلية
mainPart.Parent = nil
mainPart:Destroy()
for _, otherPart in pairs(otherParts) do
otherPart.Parent = nil
otherPart:Destroy()
end
end
تفاصيل السلوك
إذا كان الجزء الرئيسي يتحرك أثناء حساب العملية، يمكنك تعيين الأجزاء الناتجة إلى CFrame المحدث للجزء الرئيسي، حيث أن الأجزاء المرتجعة في نفس فضاء الإحداثيات مثل الجزء الرئيسي.
هناك دوال لتبديل بيانات الشبكة الخاصة بنموذج، مما يجعل من السهل الاستفادة من هندسة العملية مع الحفاظ على الخصائص، السمات، العلامات، والأطفال الخاصة بالجزء الرئيسي، مثل Attachments، Constraints، ParticleEmitters، مصابيح، وملصقات. هذه الطريقة تتجاوز أيضًا "ومضة" استبدال PartOperation الأصلية تمامًا بأخرى.
- إذا كنت تستخدم هذه الطريقة مع PartOperation كجزء رئيسي ولم تكن أي من الأجزاء الأخرى MeshParts، يمكنك استبدال هندسة نموذج آخر via SubstituteGeometry().
- إذا كان الجزء الرئيسي هو MeshPart، يمكنك استخدام MeshPart:ApplyMesh().
من الممكن استدعاء هذه الوظائف على العميل، ولكن مع بعض القيود. أولاً، يجب أن يتم ذلك باستخدام كائنات أنشئت على العميل. ثانيًا، لا توجد استنساخ متاحة من العميل إلى الخادم.
يتم تطبيق الخصائص التالية من الجزء الرئيسي على PartOperations أو MeshParts الناتجة:
- التصادم: BasePart.AudioCanCollide، BasePart.CanCollide
اعتبارات نتائج النمذجة الصلبة
الألوان و UVs
تأتي ألوان الجزء(الأجزاء) الناتجة بعد النمذجة الصلبة من مكانين: ألوان الواجهة وألوان الجزء Color.
- إذا كانت النتيجة هي PartOperation، فستمتلك Color من الجزء الأول الذي حددته في الاستوديو، ولكن يستخدم الاستوديو ألوان الواجهة افتراضيًا للحفاظ على نفس لون كل وجه كما كان قبل العملية. يمكنك تمكين خاصية UsePartColor لجعل النتيجة بالكامل لونًا واحدًا.
- إذا كانت النتيجة هي MeshPart، ستكون Color بيضاء، وستظهر ألوان الواجهة دائمًا. يمكنك ضبط صبغة الجزء(الأجزاء) الناتجة عن طريق تغيير Color، ولكن ستخلط (تضرب) مع ألوان الواجهة. هذا يُلَون النتيجة بدلاً من تجاوز ألوان الواجهة تمامًا. إذا كنت تريد التحكم الكامل في لون المخرجات، من الأفضل جعل المدخلات بيضاء أولاً.
تُعالج UVs أيضًا بشكل مختلف حسب نوع النتيجة:
- PartOperations دائمًا لديها UVs ذات تخطيط مكعب، مما يعني أن كل وجه سيكون له المادة/الملمس/الملصق من اتجاه واحد (واحد من -x، +x، -y، +y، -z، +z) تم تطبيقه عليه. هذا قد يؤدي إلى تمطيط القوام.
- MeshParts ليست مُكعبة الشكل. تُستخدم UVs من نموذج الجزء الرئيسي. نظرًا لأن روبلوكس لا تدعم حاليًا المواد المتعددة، فإن UVs للوجوه التي تنشأ من الأجزاء الأخرى تُعطى UVs (0, 0). للحصول على أفضل النتائج، تأكد من أن بيكسل (0, 0) من قماشك له لون معقول.
زاوية التنعيم
تقوم خاصية SmoothingAngle لجزء مُشكل صلب بتنعيم الزوايا بين الأسطح المجاورة من نفس اللون. تتيح القيمة الأعلى الحصول على مظهر أكثر سلاسة بينما تنتج القيمة الأقل ملمسًا خشبيًا مع حواف أكثر حدة.
بينما عادةً ما تنتج قيمة بين 30 و 70 درجة نتيجة جيدة، فإن القيم بين 90 و 180 لا يُوصى بها لأنها قد تسبب تأثير "الظل" على التحالفات والتقاطعات ذات الحواف الحادة.


تبسيط الأجزاء
إذا كانت عملية النمذجة الصلبة ستنتج أي أجزاء تحتوي على أكثر من 20,000 مثلث، فسيتم تبسيطها إلى 20,000. إذا لم يكن من الممكن القيام بذلك، عادة في حالة وجود الآلاف من المكونات غير المتداخلة، ستؤدي العملية إلى حدوث خطأ.

